* 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>
* 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>
* 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>
* 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>
* docs: fix misc remaining docs (architecture, dev quickref, sub-dir READMEs)
PLUGIN_ARCHITECTURE_SPEC.md
- Added a banner at the top noting this is a historical design doc
written before the plugin system shipped. The doc is ~1900 lines
with 13 stale /api/plugins/* paths (real is /api/v3/plugins/*),
references to web_interface_v2.py (current is app.py), and a
Migration Strategy / Implementation Roadmap that's now history.
Banner points readers at the current docs
(PLUGIN_DEVELOPMENT_GUIDE, PLUGIN_API_REFERENCE,
REST_API_REFERENCE) without needing to retrofit every section.
PLUGIN_CONFIG_ARCHITECTURE.md
- 10 occurrences of /api/plugins/* missing /v3 prefix. Bulk fixed.
DEVELOPER_QUICK_REFERENCE.md
- cache_manager.delete("key") -> cache_manager.clear_cache("key")
with comment noting delete() doesn't exist. Same bug already
documented in PLUGIN_API_REFERENCE.md.
SSH_UNAVAILABLE_AFTER_INSTALL.md
- 4 occurrences of port 5001 -> 5000 in AP-mode and Ethernet/WiFi
recovery instructions.
PLUGIN_CUSTOM_ICONS_FEATURE.md
- Port 5001 -> 5000.
CONFIG_DEBUGGING.md
- Documented /api/v3/config/plugin/<id> and /api/v3/config/validate
endpoints don't exist. Replaced with the real endpoints:
/api/v3/config/main, /api/v3/plugins/schema?plugin_id=,
/api/v3/plugins/config?plugin_id=. Added a note that validation
runs server-side automatically on POST.
STARLARK_APPS_GUIDE.md
- "Plugins -> Starlark Apps" UI navigation path doesn't exist (5
occurrences). Replaced with the real path: Plugin Manager tab,
then the per-plugin Starlark Apps tab in the second nav row.
- "Navigate to Plugins" install step -> Plugin Manager tab.
web_interface/README.md
- Documented several endpoints that don't exist in the api_v3
blueprint:
- GET /api/v3/plugins (list) -> /api/v3/plugins/installed
- GET /api/v3/plugins/<id> -> doesn't exist
- POST /api/v3/plugins/<id>/config -> POST /api/v3/plugins/config
- GET /api/v3/plugins/<id>/enable + /disable -> POST /api/v3/plugins/toggle
- GET /api/v3/store/plugins -> /api/v3/plugins/store/list
- POST /api/v3/store/install/<id> -> POST /api/v3/plugins/install
- POST /api/v3/store/uninstall/<id> -> POST /api/v3/plugins/uninstall
- POST /api/v3/store/update/<id> -> POST /api/v3/plugins/update
- POST /api/v3/display/start/stop/restart -> POST /api/v3/system/action
- GET /api/v3/display/status -> GET /api/v3/system/status
- Also fixed config/secrets.json -> config/config_secrets.json
- Replaced the per-section endpoint duplication with a current real
endpoint list and a pointer to docs/REST_API_REFERENCE.md.
- Documented that SSE stream endpoints are defined directly on the
Flask app at app.py:607-615, not in the api_v3 blueprint.
scripts/install/README.md
- Was missing 3 of the 9 install scripts in the directory:
one-shot-install.sh, configure_wifi_permissions.sh, and
debug_install.sh. Added them with brief descriptions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: clarify plugin paths and fix systemd manual install bug
PLUGIN_DEVELOPMENT_GUIDE.md
- Added a "Plugin directory note" callout near the top explaining
the plugins/ vs plugin-repos/ split:
- Dev workflow uses plugins/ (where dev_plugin_setup.sh creates
symlinks)
- Production / Plugin Store uses plugin-repos/ (the configurable
default per config.template.json:130)
- The plugin loader falls back to plugins/ so dev symlinks are
picked up automatically (schema_manager.py:77)
- User can set plugins_directory to "plugins" in the General tab
if they want both to share a directory
CLAUDE.md
- The Project Structure section had plugins/ and plugin-repos/
exactly reversed:
- Old: "plugins/ - Installed plugins directory (gitignored)"
"plugin-repos/ - Development symlinks to monorepo plugin dirs"
- Real: plugin-repos/ is the canonical Plugin Store install
location and is not gitignored. plugins/* IS gitignored
(verified in .gitignore) and is the legacy/dev location used by
scripts/dev/dev_plugin_setup.sh.
Reversed the descriptions and added line refs.
systemd/README.md
- "Manual Installation" section told users to copy the unit file
directly to /etc/systemd/system/. Verified the unit file in
systemd/ledmatrix.service contains __PROJECT_ROOT_DIR__
placeholders that the install scripts substitute at install time.
A user following the manual steps would get a service that fails
to start with "WorkingDirectory=__PROJECT_ROOT_DIR__" errors.
Added a clear warning and a sed snippet that substitutes the
placeholder before installing.
src/common/README.md
- Was missing 2 of the 11 utility modules in the directory
(verified with ls): permission_utils.py and cli.py. Added brief
descriptions for both.
Out-of-scope code bug found while auditing (flagged but not fixed):
- scripts/dev/dev_plugin_setup.sh:9 sets PROJECT_ROOT="$SCRIPT_DIR"
which resolves to scripts/dev/, not the project root. This means
the script's PLUGINS_DIR resolves to scripts/dev/plugins/ instead
of the project's plugins/ — confirmed by the existence of
scripts/dev/plugins/of-the-day/ from prior runs. Real fix is to
set PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)". Not fixing in
this docs PR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: flag aspirational/regressed features in plugin docs
These docs describe features that exist as documented in the doc but
either never wired up or regressed when v3 shipped. Each gets a clear
status banner so plugin authors don't waste time chasing features that
don't actually work.
FONT_MANAGER.md
- The "For Plugin Developers / Plugin Font Registration" section
documents adding a "fonts" block to manifest.json that gets
registered via FontManager.register_plugin_fonts(). The method
exists at src/font_manager.py:150 but is **never called from
anywhere** in the codebase (verified: zero callers). A plugin
shipping a manifest "fonts" block has its fonts silently ignored.
Added a status warning and a note about how to actually ship plugin
fonts (regular files in the plugin dir, loaded directly).
PLUGIN_IMPLEMENTATION_SUMMARY.md
- Added a top-level status banner.
- Architecture diagram referenced src/plugin_system/registry_manager.py
(which doesn't exist) and listed plugins/ as the install location.
Replaced with the real file list (plugin_loader, schema_manager,
health_monitor, operation_queue, state_manager) and pointed at
plugin-repos/ as the default install location.
- "Dependency Management: Virtual Environments" — verified there's no
per-plugin venv. Removed the bullet and added a note that plugin
Python deps install into the system Python environment, with no
conflict resolution.
- "Permission System: File Access Control / Network Access /
Resource Limits / CPU and memory constraints" — none of these
exist. There's a resource_monitor.py and health_monitor.py for
metrics/warnings, but no hard caps or sandboxing. Replaced the
section with what's actually implemented and a clear note that
plugins run in the same process with full file/network access.
PLUGIN_CUSTOM_ICONS.md and PLUGIN_CUSTOM_ICONS_FEATURE.md
- The custom-icon feature was implemented in the v2 web interface
via a getPluginIcon() helper in templates/index_v2.html that read
the manifest "icon" field. When the v3 web interface was built,
that helper wasn't ported. Verified in
web_interface/templates/v3/base.html:515 and :774, plugin tab
icons are hardcoded to `fas fa-puzzle-piece`. The "icon" field in
plugin manifests is currently silently ignored (verified with grep
across web_interface/ and src/plugin_system/ — zero non-action-
related reads of plugin.icon or manifest.icon).
- Added a status banner to both docs noting the regression so plugin
authors don't think their custom icons are broken in their own
plugin code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix .cursor/ helper docs
The .cursor/ directory holds the dev-side helper docs that Cursor and
contributors using AI tooling rely on to bootstrap plugin development.
Several of them had the same bug patterns as the user-facing docs.
.cursor/plugin_templates/QUICK_START.md
- "Adding Image Rendering" section showed
display_manager.draw_image(image, x=0, y=0). That method doesn't
exist on DisplayManager (same bug as PLUGIN_API_REFERENCE.md and
PLUGIN_DEVELOPMENT_GUIDE.md). Replaced with the canonical
display_manager.image.paste((x,y)) pattern, including the
transparency-mask form.
.cursor/plugins_guide.md
- 10 occurrences of ./dev_plugin_setup.sh — the script lives at
scripts/dev/dev_plugin_setup.sh, so anyone copy-pasting these
examples gets "command not found". Bulk fixed via sed.
- "Test with emulator: python run.py --emulator" — there's no
--emulator flag. Replaced with the real options:
EMULATOR=true python3 run.py for the full display, or
scripts/dev_server.py for the dev preview.
- Secrets management section showed a fictional
"config_secrets": { "api_key": "my-plugin.api_key" } reference
field. Verified in src/config_manager.py:162-172 that secrets are
loaded by deep-merging config_secrets.json into the main config.
There is no separate reference field — just put the secret under
the same plugin namespace and read it from the merged config.
Rewrote the section with the real pattern.
- "ssh pi@raspberrypi" -> "ssh ledpi@your-pi-ip" (consistent with
the rest of LEDMatrix docs which use ledpi as the default user)
.cursor/README.md
- Same ./dev_plugin_setup.sh -> ./scripts/dev/dev_plugin_setup.sh
fix (×6 occurrences via replace_all).
- Same "python run.py --emulator" -> "EMULATOR=true python3 run.py"
fix. Also added a pointer to scripts/dev_server.py for previewing
plugins without running the full display.
- "Example Plugins: plugins/hockey-scoreboard/" — the canonical
source is the ledmatrix-plugins repo. Installed copies land in
plugin-repos/ or plugins/. Updated the line to point at the
ledmatrix-plugins repo and explain both local locations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix .cursorrules — the file Cursor auto-loads to learn the API
This is the file that Cursor reads to learn how plugin development
works. Stale entries here directly mislead AI-assisted plugin authors
on every new plugin. Several of the same bug patterns I've been
fixing in the user-facing docs were here too.
Display Manager section (highest impact)
- "draw_image(image, x, y): Draw PIL Image" — that method doesn't
exist on DisplayManager. Same bug already fixed in
PLUGIN_API_REFERENCE.md, PLUGIN_DEVELOPMENT_GUIDE.md,
ledmatrix-stocks/README.md, and .cursor/plugin_templates/QUICK_START.md.
Removed the bullet and replaced it with a paragraph explaining the
real pattern: paste onto display_manager.image directly, then
update_display(). Includes the transparency-mask form.
- Added the small_font/centered args to draw_text() since they're
the ones that matter most for new plugin authors
- Added draw_weather_icon since it's commonly used
Cache Manager section
- "delete(key): Remove cached value" — there's no delete() method
on CacheManager. The real method is clear_cache(key=None) (also
removes everything when called without args). Same bug as before.
- Added get_cached_data_with_strategy and get_background_cached_data
since contributors will hit these when working on sports plugins
Plugin System Overview
- "loaded from the plugins/ directory" — clarified that the default
is plugin-repos/ (per config.template.json:130) with plugins/ as
the dev fallback used by scripts/dev/dev_plugin_setup.sh
Plugin Development Workflow
- ./dev_plugin_setup.sh -> ./scripts/dev/dev_plugin_setup.sh (×2)
- Manual setup step "Create directory in plugins/<plugin-id>/" ->
plugin-repos/<plugin-id>/ as the canonical location
- "Use emulator: python run.py --emulator or ./run_emulator.sh"
— the --emulator flag doesn't exist; ./run_emulator.sh isn't at
root (it lives at scripts/dev/run_emulator.sh). Replaced with the
real options: scripts/dev_server.py for dev preview, or
EMULATOR=true python3 run.py for the full emulator path.
Configuration Management
- "Reference secrets via config_secrets key in main config" — this
is the same fictional reference syntax I just fixed in
.cursor/plugins_guide.md. Verified in src/config_manager.py:162-172
that secrets are deep-merged into the main config; there's no
separate reference field. Replaced with a clear explanation of
the deep-merge approach.
Code Organization
- "plugins/<plugin-id>/" -> the canonical location is
plugin-repos/<plugin-id>/ (or its dev-time symlink in plugins/)
- "see plugins/hockey-scoreboard/ as reference" — the canonical
source for example plugins is the ledmatrix-plugins repo. Updated
the pointer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add LICENSE (GPL-3.0) and CONTRIBUTING.md
LICENSE
- The repository previously had no LICENSE file. The README and every
downstream plugin README already reference GPL-3.0 ("same as
LEDMatrix project"), but the canonical license text was missing —
contributors had no formal record of what they were contributing
under, and GitHub couldn't auto-detect the license for the repo
banner.
- Added the canonical GPL-3.0 text from
https://www.gnu.org/licenses/gpl-3.0.txt (verbatim, 674 lines).
- Compatibility verified: rpi-rgb-led-matrix is GPL-2.0-or-later
(per its COPYING file and README; the "or any later version" clause
in lib/*.h headers makes GPL-3.0 distribution legal).
CONTRIBUTING.md
- The repository had no CONTRIBUTING file. New contributors had to
reconstruct the dev setup from DEVELOPMENT.md, PLUGIN_DEVELOPMENT_GUIDE.md,
SUBMISSION.md, and the root README.
- Added a single page covering: dev environment setup (preview
server, emulator, hardware), running tests, PR submission flow,
commit message convention, plugin contribution pointer, and the
license terms contributors are agreeing to.
> Note for the maintainer: I (the AI assistant doing this audit) am
> selecting GPL-3.0 because every reference in the existing
> documentation already says GPL-3.0 — this commit just makes that
> declaration legally binding by adding the actual file. Please
> confirm during PR review that GPL-3.0 is what you want; if you
> prefer a different license, revert this commit before merging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add CODE_OF_CONDUCT, SECURITY, PR template; link them from README
Tier 1 organizational files that any open-source project at
LEDMatrix's maturity is expected to have. None of these existed
before. They're additive — no existing content was rewritten.
CODE_OF_CONDUCT.md
- Contributor Covenant 2.1 (the de facto standard for open-source
projects). Mentions both the Discord and the GitHub Security
Advisories channel for reporting violations.
SECURITY.md
- Private vulnerability disclosure flow with two channels: GitHub
Security Advisories (preferred) and Discord DM.
- Documents the project's known security model as intentional
rather than vulnerabilities: no web UI auth, plugins run
unsandboxed, display service runs as root for GPIO access,
config_secrets.json is plaintext. These match the limitations
already called out in PLUGIN_QUICK_REFERENCE.md and the audit
flagging from earlier in this PR.
- Out-of-scope section points users at upstream
(rpi-rgb-led-matrix, third-party plugins) so reports land in the
right place.
.github/PULL_REQUEST_TEMPLATE.md
- 10-line checklist that prompts for the things that would have
caught the bugs in this very PR: did you load the changed plugin
once, did you update docs alongside code, are there any plugin
compatibility implications.
- Linked from CONTRIBUTING.md for the full flow.
README.md
- Added a License section near the bottom (the README previously
said nothing about the license despite the project being GPL-3.0).
- Added a Contributing section pointing at CONTRIBUTING.md and
SECURITY.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Customize bug report template for LEDMatrix hardware
The bug_report.md template was the GitHub default and asked
"Desktop (OS/Browser/Version)" and "Smartphone (Device/OS)" — neither
of which is relevant for a project that runs on a Raspberry Pi with
hardware LED panels. A user filing a bug under the old template was
giving us none of the information we'd actually need to triage it.
Replaced with a LEDMatrix-aware template that prompts for:
- Pi model, OS/kernel, panel type, HAT/Bonnet, PWM jumper status,
display chain dimensions
- LEDMatrix git commit / release tag
- Plugin id and version (if the bug is plugin-related)
- Relevant config snippet (with redaction reminder for API keys)
- journalctl log excerpt with the exact command to capture it
- Optional photo of the actual display for visual issues
Kept feature_request.md as-is — generic content there is fine.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix bare /api/plugins paths in PLUGIN_CONFIGURATION_TABS
Found 5 more bare /api/plugins/* paths in PLUGIN_CONFIGURATION_TABS.md
that I missed in the round 2 sweep — they're inside data flow diagrams
and prose ("loaded via /api/plugins/installed", etc.) so the earlier
grep over Markdown code blocks didn't catch them. Fixed all 5 to use
/api/v3/plugins/* (the api_v3 blueprint mount path verified at
web_interface/app.py:144).
Also added a status banner noting that the "Implementation Details"
section references the pre-v3 file layout (web_interface_v2.py,
templates/index_v2.html) which no longer exists. The current
implementation is in web_interface/app.py, blueprints/api_v3.py, and
templates/v3/. Same kind of historical drift I flagged in
PLUGIN_ARCHITECTURE_SPEC.md and the PLUGIN_CUSTOM_ICONS_FEATURE doc.
The user-facing parts of the doc (Overview, Features, Form Generation
Process) are still accurate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(widgets): list the 20 undocumented built-in widgets
The widget registry README documented 3 widgets (file-upload,
checkbox-group, custom-feeds) but the directory contains 23 registered
widgets total. A plugin author reading this doc would think those 3
were the only built-in options and either reach for a custom widget
unnecessarily or settle for a generic text input.
Verified the actual list with:
grep -h "register('" web_interface/static/v3/js/widgets/*.js \
| sed -E "s|.*register\\('([^']+)'.*|\\1|" | sort -u
Added an "Other Built-in Widgets" section after the 3 detailed
sections, listing the remaining 20 with one-line descriptions
organized by category:
- Inputs (6): text-input, textarea, number-input, email-input,
url-input, password-input
- Selectors (7): select-dropdown, radio-group, toggle-switch,
slider, color-picker, font-selector, timezone-selector
- Date/time/scheduling (4): date-picker, day-selector, time-range,
schedule-picker
- Composite/data-source (2): array-table, google-calendar-picker
- Internal (2): notification, base-widget
Pointed at the .js source files as the canonical source for each
widget's exact schema and options — keeps this list low-maintenance
since I'm not duplicating each widget's full options table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix README_NBA_LOGOS and PLUGIN_CONFIGURATION_GUIDE
scripts/README_NBA_LOGOS.md
- "python download_nba_logos.py" — wrong on two counts. The script
is at scripts/download_nba_logos.py (not the project root), and
"python" is Python 2 on most systems. Replaced all 4 occurrences
with "python3 scripts/download_nba_logos.py".
- The doc framed itself as the way to set up "the NBA leaderboard".
The basketball/leaderboard functionality is now in the
basketball-scoreboard and ledmatrix-leaderboard plugins (in the
ledmatrix-plugins repo), which auto-download logos on first run.
Reframed the script as a pre-population utility for offline / dev
use cases.
- Bumped the documented Python minimum from 3.7 to 3.9 to match
the rest of the project.
docs/PLUGIN_CONFIGURATION_GUIDE.md
- The "Plugin Manifest" example was missing 3 fields the plugin
loader actually requires: id, entry_point, and class_name. A
contributor copying this manifest verbatim would get
PluginError("No class_name in manifest") at load time — the same
loader bug already found in stock-news. Added all three.
- The same example showed config_schema as an inline object. The
loader expects config_schema to be a file path string (e.g.
"config_schema.json") with the actual schema in a separate JSON
file — verified earlier in this audit. Fixed.
- Added a paragraph explaining the loader's required fields and
the case-sensitivity rule on class_name (the bug that broke
hello-world's manifest before this PR fixed it).
- "Plugin Manager Class" example had the wrong constructor
signature: (config, display_manager, cache_manager, font_manager).
The real BasePlugin.__init__ at base_plugin.py:53-60 takes
(plugin_id, config, display_manager, cache_manager, plugin_manager).
A copy-pasted example would TypeError on instantiation. Fixed,
including a comment noting which attributes BasePlugin sets up.
- Renamed the example class from MyPluginManager to MyPlugin to
match the project convention (XxxPlugin / XxxScoreboardPlugin
in actual plugins).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(requirements): document optional dependencies (scipy, psutil, Flask-Limiter)
A doc-vs-code crosscheck of every Python import in src/ and
web_interface/ against requirements.txt found 3 packages that the
code uses but requirements.txt doesn't list. Verified with grep that
all 3 are wrapped in try/except blocks with documented fallback
paths, so they're optional features rather than missing required
deps:
- scipy src/common/scroll_helper.py:26
→ from scipy.ndimage import shift; HAS_SCIPY flag.
Used for sub-pixel interpolation in scrolling.
Falls back to a simpler shift algorithm without it.
- psutil src/plugin_system/resource_monitor.py:15
→ import psutil; PSUTIL_AVAILABLE flag. Used for
per-plugin CPU/memory monitoring. Silently no-ops
without it.
- flask-limiter web_interface/app.py:42-43
→ from flask_limiter import Limiter; wrapped at the
caller. Used for accidental-abuse rate limiting on
the web interface (not security). Web interface
starts without rate limiting when missing.
These were latent in two ways:
1. A user reading requirements.txt thinks they have the full feature
set after `pip install -r requirements.txt`, but they don't get
smoother scrolling, plugin resource monitoring, or rate limiting.
2. A contributor who deletes one of the packages from their dev env
wouldn't know which feature they just lost — the fallbacks are
silent.
Added an "Optional dependencies" section at the bottom of
requirements.txt with the version constraint, the file:line where
each is used, the feature it enables, and the install command. The
comment-only format means `pip install -r requirements.txt` still
gives the minimal-feature install (preserving current behavior),
while users who want the full feature set can copy the explicit
pip install commands.
Other findings from the same scan that came back as false positives
or known issues:
- web_interface_v2: dead pattern flagged in earlier iteration
(still no real implementation; affects 11+ plugins via the same
try/except dead-fallback pattern)
- urllib3: comes with `requests` transitively
- All 'src.', 'web_interface.', 'rgbmatrix', 'RGBMatrixEmulator'
imports: internal modules
- base_plugin / plugin_manager / store_manager / mocks /
visual_display_manager: relative imports to local modules
- freetype: false positive (freetype-py is in requirements.txt
under the package name)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix broken file references found by path-existence crosscheck
Ran a doc-vs-filesystem crosscheck: extracted every backtick-quoted
path with a file extension or known directory prefix from docs/*.md
and verified each exists. After filtering false positives
(placeholder paths, config keys mistaken for paths, paths inside
docs that already have historical-status banners), found 4 real
broken references — 3 fixed in docs, 1 fixed by creating the missing
file:
docs/HOW_TO_RUN_TESTS.md:339
- Claimed ".github/workflows/tests.yml" exists and runs pytest on
multiple Python versions in CI. There is no such workflow.
The only GitHub Actions file is security-audit.yml (bandit + semgrep).
- Pytest runs locally but is NOT gated on PRs.
- Replaced the fictional CI section with the actual state and a
note explaining how someone could contribute a real test workflow.
docs/MIGRATION_GUIDE.md:92
- Referenced scripts/fix_perms/README.md "(if exists)" — the
hedge betrays that the writer wasn't sure. The README didn't
exist. The 6 scripts in scripts/fix_perms/ were never documented.
- Created the missing scripts/fix_perms/README.md from scratch
with one-line descriptions of all 6 scripts (fix_assets,
fix_cache, fix_plugin, fix_web, fix_nhl_cache, safe_plugin_rm)
+ when-to-use-each guidance + usage examples.
- Updated MIGRATION_GUIDE link to drop the "(if exists)" hedge
since the file now exists.
docs/FONT_MANAGER.md:376
- "See test/font_manager_example.py for a complete working example"
— that file does not exist. Verified by listing test/ directory.
- Replaced with a pointer to src/font_manager.py itself and the
existing scoreboard base classes in src/base_classes/ that
actually use the font manager API in production.
Path-existence check methodology:
- Walked docs/ recursively, regex-extracted backtick-quoted paths
matching either /\.(py|sh|json|yml|yaml|md|txt|service|html|js|css|ttf|bdf|png)/
or paths starting with known directory prefixes (scripts/, src/,
config/, web_interface/, systemd/, assets/, docs/, test/, etc.)
- Filtered out URLs, absolute paths (placeholders), and paths
without slashes (likely not relative refs).
- Checked existence relative to project root.
- Out of 80 unique relative paths in docs/, 32 didn't exist on
disk. Most were false positives (configkeys mistaken for paths,
example placeholders like 'assets/myfont.ttf', historical
references inside docs that already have status banners). The 4
above were genuine broken refs.
This pattern is reusable for future iterations and worth wiring
into CI (link checker like lychee, scoped to fenced code paths
rather than just markdown links, would catch the same class).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: address CodeRabbit review comments on #306
Reviewed all 12 CodeRabbit comments on PR #306, verified each against
the current code, and fixed the 11 valid ones. The 12th finding is a
real code bug (cache_manager.delete() calls in api_helper.py and
resource_monitor.py) that's already in the planned follow-up code-fix
PR, so it stays out of this docs PR.
Fixed:
.cursor/plugins_guide.md, .cursor/README.md, .cursorrules
- I claimed "there is no --emulator flag" in 3 places. Verified in
run.py:19-20 that the -e/--emulator flag is defined and functional
(it sets os.environ["EMULATOR"]="true" before the display imports).
Other docs I didn't touch (.cursor/plugin_templates/QUICK_START.md,
docs/PLUGIN_DEVELOPMENT_GUIDE.md) already use the flag correctly.
Replaced all 3 wrong statements with accurate guidance that
both forms work and explains the CLI flag's relationship to the
env var.
.cursorrules, docs/GETTING_STARTED.md, docs/WEB_INTERFACE_GUIDE.md,
docs/PLUGIN_DEVELOPMENT_GUIDE.md
- Four places claimed "the plugin loader also falls back to plugins/".
Verified that PluginManager.discover_plugins()
(src/plugin_system/plugin_manager.py:154) only scans the
configured directory — no fallback. The fallback to plugins/
exists only in two narrower places: store_manager.py:1700-1718
(store install/update/uninstall operations) and
schema_manager.py:70-80 (schema lookup for the web UI form
generator). Rewrote all four mentions with the precise scope.
Added a recommendation to set plugin_system.plugins_directory
to "plugins" for the smoothest dev workflow with
dev_plugin_setup.sh symlinks.
docs/FONT_MANAGER.md
- The "Status" warning told plugin authors to use
display_manager.font_manager.resolve_font(...) as a workaround for
loading plugin fonts. Verified in src/font_manager.py that
resolve_font() takes a family name, not a file path — so the
workaround as written doesn't actually work. Rewrote to tell
authors to load the font directly with PIL or freetype-py in their
plugin.
- The same section said "the user-facing font override system in the
Fonts tab still works for any element that's been registered via
register_manager_font()". Verified in
web_interface/blueprints/api_v3.py:5404-5428 that
/api/v3/fonts/overrides is a placeholder implementation that
returns empty arrays and contains "would integrate with the actual
font system" comments — the Fonts tab does not have functional
integration with register_manager_font() or the override system.
Removed the false claim and added an explicit note that the tab
is a placeholder.
docs/ADVANCED_FEATURES.md:523
- The on-demand section said REST/UI calls write a request "into the
cache manager (display_on_demand_config key)". Wrong — verified
via grep that api_v3.py:1622 and :1687 write to
display_on_demand_request, and display_on_demand_config is only
written by the controller during activation
(display_controller.py:1195, cleared at :1221). Corrected the key
name and added controller file:line references so future readers
can verify.
docs/ADVANCED_FEATURES.md:803
- "Plugins using the background service" paragraph listed all
scoreboard plugins but an orphaned "⏳ MLB (baseball)" bullet
remained below from the old version of the section. Removed the
orphan and added "baseball/MLB" to the inline list for clarity.
web_interface/README.md
- The POST /api/v3/system/action action list was incomplete. Verified
in web_interface/app.py:1383,1386 that enable_autostart and
disable_autostart are valid actions. Added both.
- The Plugin Store section was missing
GET /api/v3/plugins/store/github-status (verified at
api_v3.py:3296). Added it.
- The SSE line-range reference was app.py:607-615 but line 619
contains the "Exempt SSE streams from CSRF and add rate limiting"
block that's semantically part of the same feature. Extended the
range to 607-619.
docs/GETTING_STARTED.md
- Rows/Columns step said "Columns: 64 or 96 (match your hardware)".
The web UI's validation accepts any integer in 16-128. Clarified
that 64 and 96 are the common bundled-hardware values but the
valid range is wider.
Not addressed (out of scope for docs PR):
- .cursorrules:184 CodeRabbit comment flagged the non-existent
cache_manager.delete() calls in src/common/api_helper.py:287 and
src/plugin_system/resource_monitor.py:343. These are real CODE
bugs, not doc bugs, and they're the first item in the planned
post-docs-refresh code-cleanup PR (see
/home/chuck/.claude/plans/warm-imagining-river.md). The docs in
this PR correctly state that delete() doesn't exist on
CacheManager — the fix belongs in the follow-up code PR that
either adds a delete() shim or updates the two callers.
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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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 942663ab which had the complete
implementation with filtering, sorting, and pagination.
Fixes: Starlark section not visible on plugin manager page
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(starlark): restore Starlark JS functionality lost in merge
During the merge with main, all Starlark-specific JavaScript (104 lines)
was removed from plugins_manager.js, including:
- starlarkFilterState and filtering logic
- loadStarlarkApps() function
- Starlark app install/uninstall handlers
- Starlark section collapse/expand logic
- Pagination and sorting for Starlark apps
Restored from commit 942663ab and re-applied safeLocalStorage wrapper
from our code review fixes.
Fixes: Starlark Apps section non-functional in web UI
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(starlark): security and race condition improvements
Security fixes:
- Add path traversal validation for output_path in download_star_file
- Remove XSS-vulnerable inline onclick handlers, use delegated events
- Add type hints to helper functions for better type safety
Race condition fixes:
- Lock manifest file BEFORE creating temp file in _save_manifest
- Hold exclusive lock for entire read-modify-write cycle in _update_manifest_safe
- Prevent concurrent writers from racing on manifest updates
Other improvements:
- Fix pages_v3.py standalone mode to load config.json from disk
- Improve error handling with proper logging in cleanup blocks
- Add explicit type annotations to Starlark helper functions
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(starlark): critical bug fixes and code quality improvements
Critical fixes:
- Fix stack overflow in safeLocalStorage (was recursively calling itself)
- Fix duplicate event listeners on Starlark grid (added sentinel check)
- Fix JSON validation to fail fast on malformed data instead of silently passing
Error handling improvements:
- Narrow exception catches to specific types (OSError, json.JSONDecodeError, ValueError)
- Use logger.exception() with exc_info=True for better stack traces
- Replace generic "except Exception" with specific exception types
Logging improvements:
- Add "[Starlark Pixlet]" context tags to pixlet_renderer logs
- Redact sensitive config values from debug logs (API keys, etc.)
- Add file_path context to schema parsing warnings
Documentation:
- Fix markdown lint issues (add language tags to code blocks)
- Fix time unit spacing: "(5min)" -> "(5 min)"
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(starlark): critical path traversal and exception handling fixes
Path traversal security fixes (CRITICAL):
- Add _validate_starlark_app_path() helper to check for path traversal attacks
- Validate app_id in get_starlark_app(), uninstall_starlark_app(),
get_starlark_app_config(), and update_starlark_app_config()
- Check for '..' and path separators before any filesystem access
- Verify resolved paths are within _STARLARK_APPS_DIR using Path.relative_to()
- Prevents unauthorized file access via crafted app_id like '../../../etc/passwd'
Exception handling improvements (tronbyte_repository.py):
- Replace broad "except Exception" with specific types
- _make_request: catch requests.Timeout, requests.RequestException, json.JSONDecodeError
- _fetch_raw_file: catch requests.Timeout, requests.RequestException separately
- download_app_assets: narrow to OSError, ValueError
- Add "[Tronbyte Repo]" context prefix to all log messages
- Use exc_info=True for better stack traces
API improvements:
- Narrow exception catches to OSError, json.JSONDecodeError in config loading
- Remove duplicate path traversal checks (now centralized in helper)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(starlark): logging improvements and code quality fixes
Logging improvements (pages_v3.py):
- Add logging import and create module logger
- Replace print() calls with logger.warning() with "[Pages V3]" prefix
- Use logger.exception() for outer try/catch with exc_info=True
- Narrow exception handling to OSError, json.JSONDecodeError for file operations
API improvements (api_v3.py):
- Remove unnecessary f-strings (Ruff F541) from ImportError messages
- Narrow upload exception handling to ValueError, OSError, IOError
- Use logger.exception() with context for better debugging
- Remove early return in get_starlark_status() to allow standalone mode fallback
- Sanitize error messages returned to client (don't expose internal details)
Benefits:
- Better log context with consistent prefixes
- More specific exception handling prevents masking unexpected errors
- Standalone/web-service-only mode now works for status endpoint
- Stack traces preserved for debugging without exposing to clients
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(store): add sorting, filtering, and fix Update All button
Add client-side sorting and filtering to the Plugin Store:
- Sort by A-Z, Z-A, Verified First, Recently Updated, Category
- Filter by verified, new, installed status, author, and tags
- Installed/Update Available badges on store cards
- Active filter count badge with clear-all button
- Sort preference persisted to localStorage
Fix three bugs causing button unresponsiveness:
- pluginsInitialized never reset on HTMX tab navigation (root cause
of Update All silently doing nothing on second visit)
- htmx:afterSwap condition too broad (fired on unrelated swaps)
- data-running guard tied to DOM element replaced by cloneNode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(store): replace tag pills with category pills, fix sort dates
- Replace tag filter pills with category filter pills (less duplication)
- Prefer per-plugin last_updated over repo-wide pushed_at for sort
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* debug: add console logging to filter/sort handlers
* fix: bump cache-buster versions for JS and CSS
* feat(plugins): add sorting to installed plugins section
Add A-Z, Z-A, and Enabled First sort options for installed plugins
with localStorage persistence. Both installed and store sections
now default to A-Z sorting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(store): consolidate CSS, fix stale cache bug, add missing utilities, fix icon
- Consolidate .filter-pill and .category-filter-pill into shared selectors
and scope transition to only changed properties
- Fix applyStoreFiltersAndSort ignoring fresh server-filtered results by
accepting optional basePlugins parameter
- Add missing .py-1.5 and .rounded-full CSS utility classes
- Replace invalid fa-sparkles with fa-star (FA 6.0.0 compatible)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(store): semver-aware update badge and add missing gap-1.5 utility
- Replace naive version !== comparison with isNewerVersion() that does
semver greater-than check, preventing false "Update" badges on
same-version or downgrade scenarios
- Add missing .gap-1.5 CSS utility used by category pills and tag lists
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix (#249) wired window.updateAllPlugins to
PluginInstallManager.updateAll(), but that method reads from
PluginStateManager.installedPlugins which is never populated on
page load — only after individual install/update operations.
Meanwhile, base.html already defined a working updateAllPlugins
using window.installedPlugins (reliably populated by plugins_manager.js).
The override from install_manager.js masked this working version.
Fix: revert install_manager.js changes and rewrite runUpdateAllPlugins
to iterate window.installedPlugins directly, calling the API endpoint
without any middleman. Adds per-plugin progress in button text and
a summary notification on completion.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The plugin store was making excessive GitHub API calls causing slow
page loads (10-30s):
- Installed plugins endpoint called get_plugin_info() per plugin (3
GitHub API calls each) just to read the `verified` field from the
registry. Use new get_registry_info() instead (zero API calls).
- _get_latest_commit_info() had no cache — all 31 monorepo plugins
share the same repo URL, causing 31 identical API calls. Add 5-min
cache keyed by repo:branch.
- _fetch_manifest_from_github() also uncached — add 5-min cache.
- load_config() called inside loop per-plugin — hoist outside loop.
- Install/update operations pass force_refresh=True to bypass caches
and always get the latest commit SHA from GitHub.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
window.updateAllPlugins was never assigned, so the button always showed
"Bulk update handler unavailable." Wire it to PluginInstallManager.updateAll(),
add per-plugin progress feedback in the button text, show a summary
notification on completion, and skip redundant plugin list reloads.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(web): add LED RGB sequence, multiplexing, and panel type settings
Expose three rpi-rgb-led-matrix hardware options in the Display Settings
UI so users can configure non-standard panels without editing config.json
manually. All defaults match existing behavior (RGB, Direct, Standard).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): validate led_rgb_sequence, multiplexing, and panel_type inputs
Reject invalid values with 400 errors before writing to config: whitelist
check for led_rgb_sequence and panel_type, range + type check for multiplexing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(logos): support logo downloads for custom soccer leagues
LogoDownloader.fetch_teams_data() and fetch_single_team() only had
hardcoded API endpoints for predefined soccer leagues. Custom leagues
(e.g., por.1, mex.1) would silently fail when the ESPN game data
didn't include a direct logo URL. Now dynamically constructs the ESPN
teams API URL for any soccer_* league not in the predefined map.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(logos): address PR review — directory, bulk download, and dedup
- get_logo_directory: custom soccer leagues now resolve to shared
assets/sports/soccer_logos/ instead of creating per-league dirs
- download_all_missing_logos: use _resolve_api_url so custom soccer
leagues are no longer silently skipped
- Extract _resolve_api_url helper to deduplicate dynamic URL
construction between fetch_teams_data and fetch_single_team
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): add dark mode overrides for collapsible config section headers
The collapsible section headers in plugin config schemas used bg-gray-100
and hover:bg-gray-200 which had no dark mode overrides, resulting in light
text on a light background when dark mode was active.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): add missing bg-gray-100 light-mode utility class
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): add missing utility classes for log viewer readability
The log viewer uses text-gray-100, text-gray-200, text-gray-300,
text-red-300, text-yellow-300, bg-gray-800, bg-red-900, bg-yellow-900,
border-gray-700, and hover:bg-gray-800 — none of which were defined in
app.css. Without definitions, log text inherited the body's dark color
(#111827) which was invisible against the dark bg-gray-900 log container
in light mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): remove dead bg-opacity classes, use proper log level colors
The bg-opacity-10/bg-opacity-30 classes set a --bg-opacity CSS variable
that no background-color rule consumed, making them dead code. Replace
the broken two-class pattern (e.g. "bg-red-900 bg-opacity-10") with
dedicated log-level-error/warning/debug classes that use rgb() with
actual alpha values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(web): add light/dark mode toggle and fix log readability
Add a theme toggle button (moon/sun icon) to the header that switches
between light and dark mode. Theme preference persists in localStorage
and falls back to the OS prefers-color-scheme setting.
The implementation uses a data-theme attribute on <html> with CSS
overrides, so all 13 partial templates and 20+ widget JS files get
dark mode support without any modifications — only 3 files changed.
Also fixes log timestamp readability: text-gray-500 had ~3.5:1 contrast
ratio against the dark log background, now uses text-gray-400 (~5.3:1)
which passes WCAG AA in both light and dark mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): address dark mode review — accessibility, robustness, and code quality
- WCAG touch target: enforce 44×44px minimum on theme toggle button
with display:inline-flex centering
- Accessibility: add type="button", aria-pressed (dynamically updated),
aria-hidden on decorative icons, and contextual aria-label/title that
reflects current state ("Switch to light/dark mode")
- Robustness: wrap all localStorage and matchMedia calls in try/catch
with fallbacks for private browsing and restricted contexts; use
addListener fallback for older browsers lacking addEventListener
- Stylelint: convert all rgba() to modern rgb(…/…%) notation across
both light and dark theme shadows and gradients
- DRY: replace hardcoded hex values in dark mode utility overrides and
component overrides with CSS variable references (--color-surface,
--color-background, --color-border, --color-text-primary, etc.)
- Remove redundant [data-theme="dark"] body rule (body already uses
CSS variables that are redefined under the dark theme selector)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): unify operation history tracking for monorepo plugin operations
The operation history UI was reading from the wrong data source
(operation_queue instead of operation_history), install/update records
lacked version details, toggle operations used a type name that didn't
match UI filters, and the Clear History button was non-functional.
- Switch GET /plugins/operation/history to read from OperationHistory
audit log with return type hint and targeted exception handling
- Add DELETE /plugins/operation/history endpoint; wire up Clear button
- Add _get_plugin_version helper with specific exception handling
(FileNotFoundError, PermissionError, json.JSONDecodeError) and
structured logging with plugin_id/path context
- Record plugin version, branch, and commit details on install/update
- Record install failures in the direct (non-queue) code path
- Replace "toggle" operation type with "enable"/"disable"
- Add normalizeStatus() in JS to map completed→success, error→failed
so status filter works regardless of server-side convention
- Truncate commit SHAs to 7 chars in details display
- Fix HTML filter options, operation type colors, duplicate JS init
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): prevent root-owned files from blocking plugin updates
The root ledmatrix service creates __pycache__ and data cache files
owned by root inside plugin directories. The web service (non-root)
cannot delete these when updating or uninstalling plugins, causing
operations to fail with "Permission denied".
Defense in depth with three layers:
- Prevent: PYTHONDONTWRITEBYTECODE=1 in systemd service + run.py
- Fallback: sudoers rules for rm on plugin directories
- Code: _safe_remove_directory() now uses sudo as last resort,
and all bare shutil.rmtree() calls routed through it
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): harden sudo removal with path-validated helper script
Address code review findings:
- Replace raw rm/find sudoers wildcards with a vetted helper script
(safe_plugin_rm.sh) that resolves symlinks and validates the target
is a strict child of plugin-repos/ or plugins/ before deletion
- Add allow-list validation in sudo_remove_directory() that checks
resolved paths against allowed bases before invoking sudo
- Check _safe_remove_directory() return value before shutil.move()
in the manifest ID rename path
- Move stat import to module level in store_manager.py
- Use stat.S_IRWXU instead of 0o777 in chmod fallback stage
- Add ignore_errors=True to temp dir cleanup in finally block
- Use command -v instead of which in configure_web_sudo.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): address code review round 2 — harden paths and error handling
- safe_plugin_rm.sh: use realpath --canonicalize-missing for ALLOWED_BASES
so the script doesn't fail under set -e when dirs don't exist yet
- safe_plugin_rm.sh: add -- before path in rm -rf to prevent flag injection
- permission_utils.py: use shutil.which('bash') instead of hardcoded /bin/bash
to match whatever path the sudoers BASH_PATH resolves to
- store_manager.py: check _safe_remove_directory() return before shutil.move()
in _install_from_monorepo_zip to prevent moving into a non-removed target
- store_manager.py: catch OSError instead of PermissionError in Stage 1 removal
to handle both EACCES and EPERM error codes
- store_manager.py: hoist sudo_remove_directory import to module level
- configure_web_sudo.sh: harden safe_plugin_rm.sh to root-owned 755 so
the web user cannot modify the vetted helper script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): validate command paths in sudoers config and use resolved paths
- configure_web_sudo.sh: validate that required commands (systemctl, bash,
python3) resolve to non-empty paths before generating sudoers entries;
abort with clear error if any are missing; skip optional commands
(reboot, poweroff, journalctl) with a warning instead of emitting
malformed NOPASSWD lines; validate helper script exists on disk
- permission_utils.py: pass the already-resolved path to the subprocess
call and use it for the post-removal exists() check, eliminating a
TOCTOU window between Python-side validation and shell-side execution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The operation history UI was reading from the wrong data source
(operation_queue instead of operation_history), install/update records
lacked version details, toggle operations used a type name that didn't
match UI filters, and the Clear History button was non-functional.
- Switch GET /plugins/operation/history to read from OperationHistory
audit log with return type hint and targeted exception handling
- Add DELETE /plugins/operation/history endpoint; wire up Clear button
- Add _get_plugin_version helper with specific exception handling
(FileNotFoundError, PermissionError, json.JSONDecodeError) and
structured logging with plugin_id/path context
- Record plugin version, branch, and commit details on install/update
- Record install failures in the direct (non-queue) code path
- Replace "toggle" operation type with "enable"/"disable"
- Add normalizeStatus() in JS to map completed→success, error→failed
so status filter works regardless of server-side convention
- Truncate commit SHAs to 7 chars in details display
- Fix HTML filter options, operation type colors, duplicate JS init
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review nitpicks for monorepo hardening
- Add docstring note about regex limitation in parse_json_with_trailing_commas
- Abort on zip-slip in ZIP installer instead of skipping (consistent with API installer)
- Use _safe_remove_directory for non-git plugin reinstall path
- Use segment-wise encodeURIComponent for View button URL encoding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: check _safe_remove_directory result before reinstalling plugin
Avoid calling install_plugin into a partially-removed directory by
checking the boolean return of _safe_remove_directory, mirroring the
guard already used in the git-remote migration path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: normalize subpath prefix and add zip-slip guard to download installer
- Strip trailing slashes from plugin_subpath before building the tree
filter prefix, preventing double-slash ("subpath//") that would cause
file_entries to silently miss all matches.
- Add zip-slip protection to _install_via_download (extractall path),
matching the guard already present in _install_from_monorepo_zip.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: adapt LEDMatrix for monorepo plugin architecture
Update store_manager to fetch manifests from subdirectories within the
monorepo (plugin_path/manifest.json) instead of repo root. Remove 21
plugin submodule entries from .gitmodules, simplify workspace file to
reference the monorepo, and clean up scripts for the new layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: auto-reinstall plugins when registry repo URL changes
When a user clicks "Update" on a git-cloned plugin, detect if the
local git remote URL no longer matches the registry's repo URL (e.g.
after monorepo migration). Instead of pulling from the stale archived
repo, automatically remove and reinstall from the new registry source.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: plugin store "View" button links to correct monorepo subdirectory
When a plugin has a plugin_path (monorepo plugin), construct the GitHub
URL as repo/tree/main/plugin_path so users land on the specific plugin
directory. Pass plugin_path through the store API response to the
frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: monorepo manifest fetch in search + version-based update detection
Fix search_plugins() to pass plugin_path when fetching manifests from
GitHub, matching the fix already in get_plugin_info(). Without this,
monorepo plugin descriptions 404 in search results.
Add version comparison for non-git plugins (monorepo installs) so
"Update All" skips plugins already at latest_version instead of blindly
reinstalling every time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: show plugin version instead of misleading monorepo commit info
Replace commit hash, date, and stars on plugin cards with the plugin's
version number. In a monorepo all plugins share the same commit history
and star count, making those fields identical and misleading. Version
is the meaningful per-plugin signal users care about.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add CLAUDE.md with project structure and plugin store docs
Documents plugin store architecture, monorepo install flow, version-
based update detection, and the critical version bump workflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* perf: extract only target plugin from monorepo ZIP instead of all files
Previously _install_from_monorepo() called extractall() on the entire
monorepo ZIP (~13MB, 600+ files) just to grab one plugin subdirectory.
Now filter zip members by the plugin prefix and extract only matching
files, reducing disk I/O by ~96% per install/update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* perf: download only target plugin files via GitHub Trees API
Replace full monorepo ZIP download (~5MB) with targeted file downloads
(~200KB per plugin) using the GitHub Git Trees API for directory listing
and raw.githubusercontent.com for individual file content.
One API call fetches the repo tree, client filters for the target
plugin's files, then downloads each file individually. Falls back to
ZIP if the API is unavailable (rate limited, no network, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clean up partial files between API and ZIP install fallbacks
Ensure target_path is fully removed before the ZIP fallback runs, and
before shutil.move() in the ZIP method. Prevents directory nesting if
the API method creates target_path then fails mid-download.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden scripts and fix monorepo URL handling
- setup_plugin_repos.py: add type hints, remove unnecessary f-string,
wrap manifest parsing in try/except to skip malformed manifests
- update_plugin_repos.py: add 120s timeout to git pull with
TimeoutExpired handling
- store_manager.py: fix rstrip('.zip') stripping valid branch chars,
use removesuffix('.zip'); remove redundant import json
- plugins_manager.js: View button uses dynamic branch, disables when
repo is missing, encodes plugin_path in URL
- CLAUDE.md: document plugin repo naming convention
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden monorepo install security and cleanup
- store_manager: fix temp dir leak in _install_from_monorepo_zip by
moving cleanup to finally block
- store_manager: add zip-slip guard validating extracted paths stay
inside temp directory
- store_manager: add 500-file sanity cap to API-based install
- store_manager: extract _normalize_repo_url as @staticmethod
- setup_plugin_repos: propagate create_symlinks() failure via sys.exit,
narrow except to OSError
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add path traversal guard to API-based monorepo installer
Validate that each file's resolved destination stays inside
target_path before creating directories or writing bytes, mirroring
the zip-slip guard in _install_from_monorepo_zip.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use _safe_remove_directory for monorepo migration cleanup
Replace shutil.rmtree(ignore_errors=True) with _safe_remove_directory
which handles permission errors gracefully and returns status, preventing
install_plugin from running against a partially-removed directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(fonts): add dynamic font selection and font manager improvements
- Add font-selector widget for dynamic font selection in plugin configs
- Enhance /api/v3/fonts/catalog with filename, display_name, and type
- Add /api/v3/fonts/preview endpoint for server-side font rendering
- Add /api/v3/fonts/<family> DELETE endpoint with system font protection
- Fix /api/v3/fonts/upload to actually save uploaded font files
- Update font manager tab with dynamic dropdowns, server-side preview, and font deletion
- Add new BDF fonts: 6x10, 6x12, 6x13, 7x13, 7x14, 8x13, 9x15, 9x18, 10x20 (with bold/oblique variants)
- Add tom-thumb, helvR12, clR6x12, texgyre-27 fonts
Plugin authors can use x-widget: "font-selector" in schemas to enable
dynamic font selection that automatically shows all available fonts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): security fixes and code quality improvements
- Fix README.md typos and add language tags to code fences
- Remove duplicate delete_font function causing Flask endpoint collision
- Add safe integer parsing for size parameter in preview endpoint
- Fix path traversal vulnerability in /fonts/preview endpoint
- Fix path traversal vulnerability in /fonts/<family> DELETE endpoint
- Fix XSS vulnerability in fonts.html by using DOM APIs instead of innerHTML
- Move baseUrl to shared scope to fix ReferenceError in multiple functions
Security improvements:
- Validate font filenames reject path separators and '..'
- Validate paths are within fonts_dir before file operations
- Use textContent and data attributes instead of inline onclick handlers
- Restrict file extensions to known font types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): address code issues and XSS vulnerabilities
- Move `import re` to module level, remove inline imports
- Remove duplicate font_file assignment in upload_font()
- Remove redundant validation with inconsistent allowed extensions
- Remove redundant PathLib import, use already-imported Path
- Fix XSS vulnerabilities in fonts.html by using DOM APIs instead of
innerHTML with template literals for user-controlled data
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): add size limits to font preview endpoint
Add input validation to prevent DoS via large image generation:
- MAX_TEXT_CHARS (100): Limit text input length
- MAX_TEXT_LINES (3): Limit number of newlines
- MAX_DIM (1024): Limit max width/height
- MAX_PIXELS (500000): Limit total pixel count
Validates text early before processing and checks computed
dimensions after bbox calculation but before image allocation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): improve error handling, catalog keys, and BDF preview
- Add structured logging for cache invalidation failures instead of
silent pass (FontUpload, FontDelete, FontCatalog contexts)
- Use filename as unique catalog key to prevent collisions when
multiple font files share the same family_name from metadata
- Return explicit error for BDF font preview instead of showing
misleading preview with default font
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): address nitpick issues in font management
Frontend (fonts.html):
- Remove unused escapeHtml function (dead code)
- Add max-attempts guard (50 retries) to initialization loop
- Add response.ok checks before JSON parsing in deleteFont,
addFontOverride, deleteFontOverride, uploadSelectedFonts
- Use is_system flag from API instead of hardcoded client-side list
Backend (api_v3.py):
- Move SYSTEM_FONTS to module-level frozenset for single source of truth
- Add is_system flag to font catalog entries
- Simplify delete_font system font check using frozenset lookup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): align frontend upload validation with backend
- Add .otf to accepted file extensions (HTML accept attribute, JS filter)
- Update validation regex to allow hyphens (matching backend)
- Preserve hyphens in auto-generated font family names
- Update UI text to reflect all supported formats
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): fix lint errors and missing variable
- Remove unused exception binding in set_cached except block
- Define font_family_lower before case-insensitive fallback loop
- Add response.ok check to font preview fetch (consistent with other handlers)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): address nitpick code quality issues
- Add return type hints to get_font_preview and delete_font endpoints
- Catch specific PIL exceptions (IOError/OSError) when loading fonts
- Replace innerHTML with DOM APIs for trash icon (consistency)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(fonts): remove unused exception bindings in cache-clearing blocks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(plugins): prevent KeyError race condition in module cleanup
When multiple plugins have modules with the same name (e.g.,
background_data_service.py), the _clear_conflicting_modules function
could raise a KeyError if a module was removed between iteration
and deletion. This race condition caused plugin loading failures
with errors like: "Unexpected error loading plugin: 'background_data_service'"
Changes:
- Use sys.modules.pop(mod_name, None) instead of del sys.modules[mod_name]
to safely handle already-removed modules
- Apply same fix to plugin unload in plugin_manager.py for consistency
- Fix typo in sports.py: rankself._team_rankings_cacheings ->
self._team_rankings_cache
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(plugins): namespace-isolate plugin modules to prevent parallel loading collisions
Multiple sport plugins share identically-named Python files (scroll_display.py,
game_renderer.py, sports.py, etc.). When loaded in parallel via ThreadPoolExecutor,
bare module names collide in sys.modules causing KeyError crashes.
Replace _clear_conflicting_modules with _namespace_plugin_modules: after exec_module
loads a plugin, its bare-name sub-modules are moved to namespaced keys
(e.g. _plg_basketball_scoreboard_scroll_display) so they cannot collide.
A threading lock serializes the exec_module window where bare names temporarily exist.
Also updates unload_plugin to clean up namespaced sub-modules from sys.modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): address review feedback on namespace isolation
- Fix main module accidentally renamed: move before_keys snapshot to
after sys.modules[module_name] insertion so the main entry is excluded
from namespace renaming and error cleanup
- Use Path.is_relative_to() instead of substring matching for plugin
directory containment checks to avoid false-matches on overlapping
directory names
- Add try/except around exec_module to clean up partially-initialized
modules on failure, preventing leaked bare-name entries
- Add public unregister_plugin_modules() method on PluginLoader so
PluginManager doesn't reach into private attributes during unload
- Update stale comment referencing removed _clear_conflicting_modules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): remove unused plugin_dir_str variable
Leftover from the old substring containment check, now replaced by
Path.is_relative_to().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): extract shared helper for bare-module filtering
Hoist plugin_dir.resolve() out of loops and deduplicate the bare-module
filtering logic between _namespace_plugin_modules and the error cleanup
block into _iter_plugin_bare_modules().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): keep bare-name alias to prevent lazy import duplication
Stop removing bare module names from sys.modules after namespacing.
Removing them caused lazy intra-plugin imports (deferred imports inside
methods) to re-import from disk, creating a second inconsistent module
copy. Keeping both the bare and namespaced entries pointing to the same
object avoids this. The next plugin's exec_module naturally overwrites
the bare entry with its own version.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a "Shutdown System" button to the Overview page that gracefully
powers off the Raspberry Pi. Uses sudo poweroff, consistent with the
existing reboot_system action, letting sudo's secure_path handle
binary resolution.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add the led_rgb_sequence configuration option to the matrix config template,
allowing users to specify the RGB sequence for their LED panels.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): handle string boolean values in schedule-picker widget
The normalizeSchedule function used strict equality (===) to check the
enabled field, which would fail if the config value was a string "true"
instead of boolean true. This could cause the checkbox to always appear
unchecked even when the setting was enabled.
Added coerceToBoolean helper that properly handles:
- Boolean true/false (returns as-is)
- String "true", "1", "on" (case-insensitive) → true
- String "false" or other values → false
Applied to both main schedule enabled and per-day enabled fields.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: trim whitespace in coerceToBoolean string handling
* fix: normalize mode value to handle per_day and per-day variants
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): handle string boolean values in schedule-picker widget
The normalizeSchedule function used strict equality (===) to check the
enabled field, which would fail if the config value was a string "true"
instead of boolean true. This could cause the checkbox to always appear
unchecked even when the setting was enabled.
Added coerceToBoolean helper that properly handles:
- Boolean true/false (returns as-is)
- String "true", "1", "on" (case-insensitive) → true
- String "false" or other values → false
Applied to both main schedule enabled and per-day enabled fields.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: trim whitespace in coerceToBoolean string handling
* fix: normalize mode value to handle per_day and per-day variants
* fix(plugins): resolve module namespace collisions between plugins
When multiple plugins have modules with the same name (e.g., data_fetcher.py),
Python's sys.modules cache would return the wrong module. This caused plugins
like ledmatrix-stocks to fail loading because it imported data_fetcher from
ledmatrix-leaderboard instead of its own.
Added _clear_conflicting_modules() to remove cached plugin modules from
sys.modules before loading each plugin, ensuring correct module resolution.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): handle string boolean values in schedule-picker widget
The normalizeSchedule function used strict equality (===) to check the
enabled field, which would fail if the config value was a string "true"
instead of boolean true. This could cause the checkbox to always appear
unchecked even when the setting was enabled.
Added coerceToBoolean helper that properly handles:
- Boolean true/false (returns as-is)
- String "true", "1", "on" (case-insensitive) → true
- String "false" or other values → false
Applied to both main schedule enabled and per-day enabled fields.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: trim whitespace in coerceToBoolean string handling
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): exclude rpi-rgb-led-matrix from permission normalization
The permission normalization step in first_time_install.sh was running
chmod 644 on all files, which stripped executable bits from compiled
library files (librgbmatrix.so.1) after make build-python created them.
This caused LED panels to not work after fresh installation until users
manually ran chmod on the rpi-rgb-led-matrix-master directory.
Fixes#224
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): resolve install script issues and speed up web UI startup
Issues addressed:
- Remove redundant python3-pillow from apt (Debian maps it to python3-pil)
- Only upgrade pip, not setuptools/wheel (they conflict with apt versions)
- Remove separate apt numpy install (pip handles it from requirements.txt)
- Install web interface deps during first-time setup, not on every startup
- Add marker file (.web_deps_installed) to skip redundant pip installs
- Add user-friendly message about wait time after installation
The web UI was taking 30-60+ seconds to start because it ran pip install
on every startup. Now it only installs dependencies on first run.
Fixes#208
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): prevent duplicate web dependency installation
Step 7 was installing web dependencies again even though they were
already installed in Step 5. Now Step 7 checks for the .web_deps_installed
marker file and skips the installation if it already exists.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): only create web deps marker on successful install
The .web_deps_installed marker file should only be created when pip
install actually succeeds. Previously it was created regardless of
the pip exit status, which could cause subsequent runs to skip
installing missing dependencies.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): create config files before starting services
The services were being started before config files existed, causing
the web service to fail with "config.json not found". Reordered steps
so config files are created (Step 4) before services are installed
and started (Step 4.1).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): remove pip upgrade step (apt version is sufficient)
The apt-installed pip cannot be upgraded because it doesn't have a
RECORD file. Since the apt version (25.1.1) is already recent enough,
we can skip the upgrade step entirely.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): reorder script to install services after dependencies
Moved main LED Matrix service installation (Step 7.5) to after all
Python dependencies are installed (Steps 5-7). Previously services
were being started before pip packages and rgbmatrix were ready,
causing startup failures.
New order:
- Step 5: Python pip dependencies
- Step 6: rpi-rgb-led-matrix build
- Step 7: Web interface dependencies
- Step 7.5: Main LED Matrix service (moved here)
- Step 8: Web interface service
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): update step list and fix setcap symlink handling
- Updated step list header to match actual step order after reordering
(Step 4 is now "Ensure configuration files exist", added Step 7.5
for main service, added Step 8.1 for systemd permissions)
- Fixed Python capabilities configuration:
- Check if setcap command exists before attempting to use it
- Resolve symlinks with readlink -f to get the real binary path
- Only print success message when setcap actually succeeds
- Print clear warning with helpful info when setcap fails
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): exclude rpi-rgb-led-matrix from permission normalization
The permission normalization step in first_time_install.sh was running
chmod 644 on all files, which stripped executable bits from compiled
library files (librgbmatrix.so.1) after make build-python created them.
This caused LED panels to not work after fresh installation until users
manually ran chmod on the rpi-rgb-led-matrix-master directory.
Fixes#224
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): resolve install script issues and speed up web UI startup
Issues addressed:
- Remove redundant python3-pillow from apt (Debian maps it to python3-pil)
- Only upgrade pip, not setuptools/wheel (they conflict with apt versions)
- Remove separate apt numpy install (pip handles it from requirements.txt)
- Install web interface deps during first-time setup, not on every startup
- Add marker file (.web_deps_installed) to skip redundant pip installs
- Add user-friendly message about wait time after installation
The web UI was taking 30-60+ seconds to start because it ran pip install
on every startup. Now it only installs dependencies on first run.
Fixes#208
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): prevent duplicate web dependency installation
Step 7 was installing web dependencies again even though they were
already installed in Step 5. Now Step 7 checks for the .web_deps_installed
marker file and skips the installation if it already exists.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(install): only create web deps marker on successful install
The .web_deps_installed marker file should only be created when pip
install actually succeeds. Previously it was created regardless of
the pip exit status, which could cause subsequent runs to skip
installing missing dependencies.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove plugin-specific calendar duration from config template
Plugin display durations should be added dynamically when plugins are
installed, not hardcoded in the template.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): ensure unchecked checkboxes save as false in main config forms
HTML checkboxes omit their key entirely when unchecked, so the backend
never received updates to set boolean values to false. This affected:
- vegas_scroll_enabled: Now uses _coerce_to_bool helper
- use_short_date_format: Now uses _coerce_to_bool helper
- Plugin system checkboxes (auto_discover, auto_load_enabled, development_mode):
Now uses _coerce_to_bool helper
- Hardware checkboxes (disable_hardware_pulsing, inverse_colors, show_refresh_rate):
Now uses _coerce_to_bool helper
- web_display_autostart: Now uses _coerce_to_bool helper
Added _coerce_to_bool() helper function that properly converts form string
values ("true", "on", "1", "yes") to actual Python booleans, ensuring
consistent JSON types in config and correct downstream boolean checks.
Also added value="true" to all main config checkboxes for consistent boolean
parsing (sends "true" instead of "on" when checked).
This is the same issue fixed in commit 10d70d91 for plugin configs, but
for the main configuration forms (display settings, general settings).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Reduce max_steps from 0.1s to 0.04s of catch-up time (from 5 to 2 steps
at 50 FPS). When the system lags, the previous catch-up logic allowed
jumping up to 5 pixels at once, causing visible jitter. Limiting to 2
steps provides smoother scrolling while still allowing for minor timing
corrections.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Plugin display durations should be added dynamically when plugins are
installed, not hardcoded in the template.
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add timezone support for schedules and dim schedule feature
- Fix timezone handling in _check_schedule() to use configured timezone
instead of system time (addresses schedule offset issues)
- Add dim schedule feature for automatic brightness dimming:
- New dim_schedule config section with brightness level and time windows
- Smart interaction: dim schedule won't turn display on if it's off
- Supports both global and per-day modes like on/off schedule
- Add set_brightness() and get_brightness() methods to DisplayManager
for runtime brightness control
- Add REST API endpoints: GET/POST /api/v3/config/dim-schedule
- Add web UI for dim schedule configuration in schedule settings page
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: normalize per-day mode and validate dim_brightness input
- Normalize mode string in _check_dim_schedule to handle both "per-day"
and "per_day" variants
- Add try/except around dim_brightness int conversion to handle invalid
input gracefully
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: improve error handling in brightness and dim schedule endpoints
- display_manager.py: Add fail-fast input validation, catch specific
exceptions (AttributeError, TypeError, ValueError), add [BRIGHTNESS]
context tags, include stack traces in error logs
- api_v3.py: Catch specific config exceptions (FileNotFoundError,
JSONDecodeError, IOError), add [DIM SCHEDULE] context tags for
Pi debugging, include stack traces
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(display): add Vegas-style continuous scroll mode
Implement an opt-in Vegas ticker mode that composes all enabled plugin
content into a single continuous horizontal scroll. Includes a modular
package (src/vegas_mode/) with double-buffered streaming, 125 FPS
render pipeline using the existing ScrollHelper, live priority
interruption support, and a web UI for configuration with drag-drop
plugin ordering.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(vegas): add three-mode display system (SCROLL, FIXED_SEGMENT, STATIC)
Adds a flexible display mode system for Vegas scroll mode that allows
plugins to control how their content appears in the continuous scroll:
- SCROLL: Content scrolls continuously (multi-item plugins like sports)
- FIXED_SEGMENT: Fixed block that scrolls by (clock, weather)
- STATIC: Scroll pauses, plugin displays, then resumes (alerts)
Changes:
- Add VegasDisplayMode enum to base_plugin.py with backward-compatible
mapping from legacy get_vegas_content_type()
- Add static pause handling to coordinator with scroll position save/restore
- Add mode-aware content composition to stream_manager
- Add vegas_mode info to /api/v3/plugins/installed endpoint
- Add mode indicators to Vegas settings UI
- Add comprehensive plugin developer documentation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas,widgets): address validation, thread safety, and XSS issues
Vegas mode fixes:
- config.py: align validation limits with UI (scroll_speed max 200, separator_width max 128)
- coordinator.py: fix race condition by properly initializing _pending_config
- plugin_adapter.py: remove unused import
- render_pipeline.py: preserve deque type in reset() method
- stream_manager.py: fix lock handling and swap_buffers to truly swap
API fixes:
- api_v3.py: normalize boolean checkbox values, validate numeric fields, ensure JSON arrays
Widget fixes:
- day-selector.js: remove escapeHtml from JSON.stringify to prevent corruption
- password-input.js: use deterministic color class mapping for Tailwind JIT
- radio-group.js: replace inline onchange with addEventListener to prevent XSS
- select-dropdown.js: guard global registry access
- slider.js: add escapeAttr for attributes, fix null dereference in setValue
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): improve exception handling and static pause state management
coordinator.py:
- _check_live_priority: use logger.exception for full traceback
- _end_static_pause: guard scroll resume on interruption (stop/live priority)
- _update_static_mode_plugins: log errors instead of silently swallowing
render_pipeline.py:
- compose_scroll_content: use specific exceptions and logger.exception
- render_frame: use specific exceptions and logger.exception
- hot_swap_content: use specific exceptions and logger.exception
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): add interrupt mechanism and improve config/exception handling
- Add interrupt checker callback to Vegas coordinator for responsive
handling of on-demand requests and wifi status during Vegas mode
- Fix config.py update() to include dynamic duration fields
- Fix is_plugin_included() consistency with get_ordered_plugins()
- Update _apply_pending_config to propagate config to StreamManager
- Change _fetch_plugin_content to use logger.exception for traceback
- Replace bare except in _refresh_plugin_list with specific exceptions
- Add aria-label accessibility to Vegas toggle checkbox
- Fix XSS vulnerability in plugin metadata rendering with escapeHtml
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): improve logging, validation, lock handling, and config updates
- display_controller.py: use logger.exception for Vegas errors with traceback
- base_plugin.py: validate vegas_panel_count as positive integer with warning
- coordinator.py: fix _apply_pending_config to avoid losing concurrent updates
by clearing _pending_config while holding lock
- plugin_adapter.py: remove broad catch-all, use narrower exception types
(AttributeError, TypeError, ValueError, OSError, RuntimeError) and
logger.exception for traceback preservation
- api_v3.py: only update vegas_config['enabled'] when key is present in data
to prevent incorrect disabling when checkbox is omitted
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): improve cycle advancement, logging, and accessibility
- Add advance_cycle() method to StreamManager for clearing buffer between cycles
- Call advance_cycle() in RenderPipeline.start_new_cycle() for fresh content
- Use logger.exception() for interrupt check and static pause errors (full tracebacks)
- Add id="vegas_scroll_label" to h3 for aria-labelledby reference
- Call updatePluginConfig() after rendering plugin list for proper initialization
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): add thread-safety, preserve updates, and improve logging
- display_controller.py: Use logger.exception() for Vegas import errors
- plugin_adapter.py: Add thread-safe cache lock, remove unused exception binding
- stream_manager.py: In-place merge in process_updates() preserves non-updated plugins
- api_v3.py: Change vegas_scroll_enabled default from False to True
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): add debug logging and narrow exception types
- stream_manager.py: Log when get_vegas_display_mode() is unavailable
- stream_manager.py: Narrow exception type from Exception to (AttributeError, TypeError)
- api_v3.py: Log exceptions when reading Vegas display metadata with plugin context
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): fix method call and improve exception logging
- Fix _check_vegas_interrupt() calling nonexistent _check_wifi_status(),
now correctly calls _check_wifi_status_message()
- Update _refresh_plugin_list() exception handler to use logger.exception()
with plugin_id and class name for remote debugging
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): replace complex toggle with standard checkbox for Vegas mode
The Tailwind pseudo-element toggle (after:content-[''], etc.) wasn't
rendering because these classes weren't in the CSS bundle. Replaced
with a simple checkbox that matches other form controls in the template.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* debug(vegas): add detailed logging to _refresh_plugin_list
Track why plugins aren't being found for Vegas scroll:
- Log count of loaded plugins
- Log enabled status for each plugin
- Log content_type and display_mode checks
- Log when plugin_manager lacks loaded_plugins
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): use correct attribute name for plugin manager
StreamManager and VegasModeCoordinator were checking for
plugin_manager.loaded_plugins but PluginManager stores active
plugins in plugin_manager.plugins. This caused Vegas scroll
to find zero plugins despite plugins being available.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): convert scroll_speed from px/sec to px/frame correctly
The config scroll_speed is in pixels per second, but ScrollHelper
in frame_based_scrolling mode interprets it as pixels per frame.
Previously this caused the speed to be clamped to max 5.0 regardless
of the configured value.
Now properly converts: pixels_per_frame = scroll_speed * scroll_delay
With defaults (50 px/s, 0.02s delay), this gives 1 px/frame = 50 px/s.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(vegas): add FPS logging every 5 seconds
Logs actual FPS vs target FPS to help diagnose performance issues.
Shows frame count in each 5-second interval.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): improve plugin content capture reliability
- Call update_data() before capture to ensure fresh plugin data
- Try display() without force_clear first, fallback if TypeError
- Retry capture with force_clear=True if first attempt is blank
- Use histogram-based blank detection instead of point sampling
(more reliable for content positioned anywhere in frame)
This should help capture content from plugins that don't implement
get_vegas_content() natively.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): handle callable width/height on display_manager
DisplayManager.width and .height may be methods or properties depending
on the implementation. Use callable() check to call them if needed,
ensuring display_width and display_height are always integers.
Fixes potential TypeError when width/height are methods.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): use logger.exception for display mode errors
Replace logger.error with logger.exception to capture full stack trace
when get_vegas_display_mode() fails on a plugin.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): protect plugin list updates with buffer lock
Move assignment of _ordered_plugins and index resets under _buffer_lock
to prevent race conditions with _prefetch_content() which reads these
variables under the same lock.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): catch all exceptions in get_vegas_display_mode
Broaden exception handling from AttributeError/TypeError to Exception
so any plugin error in get_vegas_display_mode() doesn't abort the
entire plugin list refresh. The loop continues with the default
FIXED_SEGMENT mode.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(vegas): refresh stream manager when config updates
After updating stream_manager.config, force a refresh to pick up changes
to plugin_order, excluded_plugins, and buffer_ahead settings. Also use
logger.exception to capture full stack traces on config update errors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* debug(vegas): add detailed logging for blank image detection
* feat(vegas): extract full scroll content from plugins using ScrollHelper
Plugins like ledmatrix-stocks and odds-ticker use ScrollHelper with a
cached_image that contains their full scrolling content. Instead of
falling back to single-frame capture, now check for scroll_helper.cached_image
first to get the complete scrolling content for Vegas mode.
* debug(vegas): add comprehensive INFO-level logging for plugin content flow
- Log each plugin being processed with class name
- Log which content methods are tried (native, scroll_helper, fallback)
- Log success/failure of each method with image dimensions
- Log brightness check results for blank image detection
- Add visual separators in logs for easier debugging
- Log plugin list refresh with enabled/excluded status
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(vegas): trigger scroll content generation when cache is empty
When a plugin has a scroll_helper but its cached_image is not yet
populated, try to trigger content generation by:
1. Calling _create_scrolling_display() if available (stocks pattern)
2. Calling display(force_clear=True) as a fallback
This allows plugins like stocks to provide their full scroll content
even when Vegas mode starts before the plugin has run its normal
display cycle.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: improve exception handling in plugin_adapter scroll content retrieval
Replace broad except Exception handlers with narrow exception types
(AttributeError, TypeError, ValueError, OSError) and use logger.exception
instead of logger.warning/info to capture full stack traces for better
diagnosability.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: narrow exception handling in coordinator and plugin_adapter
- coordinator.py: Replace broad Exception catch around get_vegas_display_mode()
with (AttributeError, TypeError) and use logger.exception for stack traces
- plugin_adapter.py: Narrow update_data() exception handler to
(AttributeError, RuntimeError, OSError) and use logger.exception
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: improve Vegas mode robustness and API validation
- display_controller: Guard against None plugin_manager in Vegas init
- coordinator: Restore scrolling state in resume() to match pause()
- api_v3: Validate Vegas numeric fields with range checks and 400 errors
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): ensure unchecked boolean checkboxes save as false
HTML checkboxes don't submit values when unchecked. The plugin config
save endpoint starts from existing config (for partial updates), so an
unchecked checkbox's old `true` value persists. Additionally,
merge_with_defaults fills in schema defaults for missing fields, causing
booleans with `"default": true` to always re-enable.
This affected the odds-ticker plugin where NFL/NBA leagues (default:
true) could not be disabled via the checkbox UI, while NHL (default:
false) appeared to work by coincidence.
Changes:
- Add _set_missing_booleans_to_false() that walks the schema after form
processing and sets any boolean field absent from form data to false
- Add value="true" to boolean checkboxes so checked state sends "true"
instead of "on" (proper boolean parsing)
- Handle "on"/"off" strings in _parse_form_value_with_schema for
backwards compatibility with checkboxes lacking value="true"
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): guard on/off coercion to boolean schema types, handle arrays
- Only coerce "on"/"off" strings to booleans when the schema type is
boolean; "true"/"false" remain unconditional
- Extend _set_missing_booleans_to_false to recurse into arrays of
objects (e.g. custom_feeds.0.enabled) by discovering item indices
from submitted form keys and recursing per-index
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): preserve array structures when setting missing booleans
_set_nested_value uses dict-style access for all path segments, which
corrupts lists when paths contain numeric array indices (e.g.
"feeds.custom_feeds.0.enabled").
Refactored _set_missing_booleans_to_false to:
- Accept an optional config_node parameter for direct array item access
- When inside an array item, set booleans directly on the item dict
- Navigate to array lists manually, preserving their list type
- Ensure array items exist as dicts before recursing
This prevents array-of-object configs (like custom_feeds) from being
converted to nested dicts with numeric string keys.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(widgets): add modular widget system for schedule and common inputs
Add 15 new reusable widgets following the widget registry pattern:
- schedule-picker: composite widget for enable/mode/time configuration
- day-selector: checkbox group for days of the week
- time-range: paired start/end time inputs with validation
- text-input, number-input, textarea: enhanced text inputs
- toggle-switch, radio-group, select-dropdown: selection widgets
- slider, color-picker, date-picker: specialized inputs
- email-input, url-input, password-input: validated string inputs
Refactor schedule.html to use the new schedule-picker widget instead
of inline JavaScript. Add x-widget support in plugin_config.html for
all new widgets so plugins can use them via schema configuration.
Fix form submission for checkboxes by using hidden input pattern to
ensure unchecked state is properly sent via JSON-encoded forms.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): improve security, validation, and form binding across widgets
- Fix XSS vulnerability: escapeHtml now escapes quotes in all widget fallbacks
- color-picker: validate presets with isValidHex(), use data attributes
- date-picker: add placeholder attribute support
- day-selector: use options.name for hidden input form binding
- password-input: implement requireUppercase/Number/Special validation
- radio-group: fix value injection using this.value instead of interpolation
- schedule-picker: preserve day values when disabling (don't clear times)
- select-dropdown: remove undocumented searchable/icons options
- text-input: apply patternMessage via setCustomValidity
- time-range: use options.name for hidden inputs
- toggle-switch: preserve configured color from data attribute
- url-input: combine browser and custom protocol validation
- plugin_config: add widget support for boolean/number types, pass name to day-selector
- schedule: handle null config gracefully, preserve explicit mode setting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): validate day-selector input, consistent minLength default, escape JSON quotes
- day-selector: filter incoming selectedDays to only valid entries in DAYS array
(prevents invalid persisted values from corrupting UI/state)
- password-input: use default minLength of 8 when not explicitly set
(fixes inconsistency between render() and onInput() strength meter baseline)
- plugin_config.html: escape single quotes in JSON hidden input values
(prevents broken attributes when JSON contains single quotes)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(widgets): add global notification widget, consolidate duplicated code
- Create notification.js widget with toast-style notifications
- Support for success, error, warning, info types
- Auto-dismiss with configurable duration
- Stacking support with max notifications limit
- Accessible with aria-live and role="alert"
- Update base.html to load notification widget early
- Replace duplicate showNotification in raw_json.html
- Simplify fonts.html fallback notification
- Net reduction of ~66 lines of duplicated code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): escape options.name in all widgets, validate day-selector format
Security fixes:
- Escape options.name attribute in all 13 widgets to prevent injection
- Affected: color-picker, date-picker, email-input, number-input,
password-input, radio-group, select-dropdown, slider, text-input,
textarea, toggle-switch, url-input
Defensive coding:
- day-selector: validate format option exists in DAY_LABELS before use
- Falls back to 'long' format for unsupported/invalid format values
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(plugins): add type="button" to control buttons, add debug logging
- Add type="button" attribute to refresh, update-all, and restart buttons
to prevent potential form submission behavior
- Add console logging to diagnose button click issues:
- Log when event listeners are attached (and whether buttons found)
- Log when handler functions are called
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): improve security and validation across widget inputs
- color-picker.js: Add sanitizeHex() to validate hex values before HTML
interpolation, ensuring only safe #rrggbb strings are used
- day-selector.js: Escape inputName in hidden input name attribute
- number-input.js: Sanitize and escape currentValue in input element
- password-input.js: Validate minLength as non-negative integer, clamp
invalid values to default of 8
- slider.js: Add null check for input element before accessing value
- text-input.js: Clear custom validity before checkValidity() to avoid
stale errors, re-check after setting pattern message
- url-input.js: Normalize allowedProtocols to array, filter to valid
protocol strings, and escape before HTML interpolation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): add defensive fallback for DAY_LABELS lookup in day-selector
Extract labelMap with fallback before loop to ensure safe access even if
format validation somehow fails.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(widgets): add timezone-selector widget with IANA timezone dropdown
- Create timezone-selector.js widget with comprehensive IANA timezone list
- Group timezones by region (US & Canada, Europe, Asia, etc.)
- Show current UTC offset for each timezone
- Display live time preview for selected timezone
- Update general.html to use timezone-selector instead of text input
- Add script tag to base.html for widget loading
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): suppress on-demand status notification on page load
Change loadOnDemandStatus(true) to loadOnDemandStatus(false) during
initPluginsPage() to prevent the "on-demand status refreshed"
notification from appearing every time a tab is opened or the page
is navigated. The notification should only appear on explicit user
refresh.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style(ui): soften notification close button appearance
Replace blocky FontAwesome X icon with a cleaner SVG that has rounded
stroke caps. Make the button circular, slightly transparent by default,
and add smooth hover transitions for a more polished look.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): multiple security and validation improvements
- color-picker.js: Ensure presets is always an array before map/filter
- number-input.js: Guard against undefined options parameter
- number-input.js: Sanitize and escape min/max/step HTML attributes
- text-input.js: Clear custom validity in onInput to unblock form submit
- timezone-selector.js: Replace legacy Europe/Belfast with Europe/London
- url-input.js: Use RFC 3986 scheme pattern for protocol validation
- general.html: Use |tojson filter to escape timezone value safely
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(url-input): centralize RFC 3986 protocol validation
Extract protocol normalization into reusable normalizeProtocols()
helper function that validates against RFC 3986 scheme pattern.
Apply consistently in render, validate, and onInput to ensure
protocols like "git+ssh", "android-app" are properly handled
everywhere. Also lowercase protocol comparison in isValidUrl().
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(timezone-selector): use hidden input for form submission
Replace direct select name attribute with a hidden input pattern to
ensure timezone value is always properly serialized in form submissions.
The hidden input is synced on change and setValue calls. This matches
the pattern used by other widgets and ensures HTMX json-enc properly
captures the value.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(general): preserve timezone dropdown value after save
Add inline script to sync the timezone select with the hidden input
value after form submission. This prevents the dropdown from visually
resetting to the old value while the save has actually succeeded.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): preserve timezone selection across form submission
Use before-request handler to capture the selected timezone value
before HTMX processes the form, then restore it in after-request.
This is more robust than reading from the hidden input which may
also be affected by form state changes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): add HTMX protection to timezone selector
Add global HTMX event listeners in the timezone-selector widget
that preserve the selected value across any form submissions.
This is more robust than form-specific handlers as it protects
the widget regardless of how/where forms are submitted.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* debug(widgets): add logging and prevent timezone widget re-init
Add debug logging and guards to prevent the timezone widget from
being re-initialized after it's already rendered. This should help
diagnose why the dropdown is reverting after save.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* debug: add console logging to timezone HTMX protection
* debug: add onChange logging to trace timezone selection
* fix(widgets): use selectedIndex to force visual update in timezone dropdown
The browser's select.value setter sometimes doesn't trigger a visual
update when optgroup elements are present. Using selectedIndex instead
forces the browser to correctly update the visible selection.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): force browser repaint on timezone dropdown restore
Adding display:none/reflow/display:'' pattern to force browser to
visually update the select element after changing selectedIndex.
Increased timeout to 50ms for reliability.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore(widgets): remove debug logging from timezone selector
Clean up console.log statements that were used for debugging the
timezone dropdown visual update issue.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): improve HTMX after-request handler in general settings
- Parse xhr.responseText with JSON.parse in try/catch instead of
using nonstandard responseJSON property
- Check xhr.status for 2xx success range
- Show error notification for non-2xx responses
- Default to safe fallback values if JSON parsing fails
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): add input sanitization and timezone validation
- Sanitize minLength/maxLength in text-input.js to prevent attribute
injection (coerce to integers, validate range)
- Update Europe/Kiev to Europe/Kyiv (canonical IANA identifier)
- Validate timezone currentValue against TIMEZONE_GROUPS before rendering
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): correct error message fallback in HTMX after-request handler
Initialize message to empty string so error responses can use the
fallback 'Failed to save settings' when no server message is provided.
Previously, the truthy default 'Settings saved' would always be used.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): add constraint normalization and improve value validation
- text-input: normalize minLength/maxLength so maxLength >= minLength
- timezone-selector: validate setValue input against TIMEZONE_GROUPS
- timezone-selector: sync hidden input to actual selected value
- timezone-selector: preserve empty selections across HTMX requests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(widgets): simplify HTMX restore using select.value and dispatch change event
Replace selectedIndex manipulation with direct value assignment for cleaner
placeholder handling, and dispatch change event to refresh timezone preview.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Issue: LEDMatrix was changing /tmp permissions from 1777 (drwxrwxrwt)
to 2775 (drwxrwsr-x), breaking apt update and other system tools.
Root cause: display_manager.py's _write_snapshot_if_due() called
ensure_directory_permissions() on /tmp when writing snapshots to
/tmp/led_matrix_preview.png. This removed the sticky bit and
world-writable permissions that /tmp requires.
Fix:
- Added PROTECTED_SYSTEM_DIRECTORIES safelist to permission_utils.py
to prevent modifying permissions on /tmp and other system directories
- Added explicit check in display_manager.py to skip /tmp
- Defense-in-depth approach prevents similar issues in other code paths
The sticky bit (1xxx) is critical for /tmp - it prevents users from
deleting files they don't own. Without world-writable permissions,
regular users cannot create temp files.
Fixes#202
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(ui): wrap plugin tabs to new lines instead of scrolling
Change plugin tabs row from overflow-x-auto to flex-wrap so that
when many plugins are installed, tabs break to new lines instead
of becoming smaller or requiring horizontal scrolling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): use gap-x instead of space-x for proper wrapped row alignment
Switch from space-x-* to gap-x-* utilities so wrapped rows align
correctly without indentation on subsequent lines.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): add missing flex-wrap and gap utilities to CSS
The project uses hand-written Tailwind-like CSS, not actual Tailwind.
Added missing utility classes needed for plugin tabs wrapping:
- flex-wrap
- gap-x-4, gap-x-6, gap-x-8, gap-y-2
- lg:gap-x-6, xl:gap-x-8 responsive variants
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): apply flex-wrap to system tabs row
Apply the same wrapping behavior to the system tabs row (Overview,
General, WiFi, etc.) so they also wrap to new lines on smaller
viewports instead of scrolling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): constrain tab container width to enable flex-wrap
Add max-w-full and overflow-hidden to tab row containers to properly
constrain their width, allowing flex-wrap to work correctly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): remove overflow-hidden that was hiding tabs
Revert the max-w-full overflow-hidden approach as it was hiding
content. Keep both rows using flex-wrap with gap utilities.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: Add custom-leagues widget support for soccer plugin
- Add server-side template rendering for x-widget="custom-leagues"
- Renders table with Name, League Code, Priority, Enabled columns
- Includes inline JavaScript for add/remove row functionality
- Uses indexed field naming for proper array serialization
- Shows common ESPN league codes as hint
This enables the soccer scoreboard plugin's custom leagues feature
to work properly in the web UI.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): reduce tab gap spacing for tighter layout
Reduce horizontal gap between tabs from gap-x-4/6/8 to gap-x-2/3/4
for a more compact appearance.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(widget): Replace custom-leagues with generic array-table widget
- Add generic array-table widget that reads columns from schema
- Support x-columns to specify which columns to display
- Auto-detect columns from items.properties if x-columns not specified
- Remove hardcoded custom-leagues implementation
- Any plugin can now use x-widget: "array-table" for array-of-objects
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): use data attributes for array table button to avoid JSON escaping issues
Move JSON blobs (item_properties and display_columns) from inline onclick
to data-* attributes with proper HTML entity escaping via Jinja's |e filter.
Update addArrayTableRow() to read and parse these data attributes.
This fixes HTML attribute breakage caused by tojson emitting double quotes
inside the onclick attribute value.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): update Add button state when array table rows change
Add updateAddButtonState() helper that toggles the Add button's disabled
attribute and opacity based on current row count vs maxItems.
Called after addArrayTableRow() and removeArrayTableRow(), and also on
page load to ensure correct initial state.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): add try/catch for JSON parsing in addArrayTableRow
Wrap JSON.parse calls for data-item-properties and data-display-columns
in try/catch blocks with fallback to {} and [] respectively. Logs error
with raw attribute values to help debug malformed JSON.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(array-table): Fix getValue input name validation and setValue Add button state sync
- Fix getValue to use early-continue guard preventing errors on inputs without names
- Add updateAddButtonState call in setValue to refresh Add button state after repopulating rows
* fix(ui): make Configure button larger than Uninstall in plugin manager
Swapped button sizes in installed plugins section - Configure button is now
the largest (flex-2), Update is medium (flex-1), and Uninstall is smallest
(no flex class). This prioritizes the Configure action over Uninstall.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(ui): correct forEach continue and plugin button flex sizing
- Replace invalid continue with return in array-table forEach callback
- Remove redundant hidden input type check in array-table getValue
- Fix plugin button sizing using inline flex styles instead of invalid flex-2 class
- Configure button now properly sized at flex: 2, Update and Uninstall at flex: 1
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* refactor(ui): reorganize plugin buttons into two-row layout
Configure button now takes full width on first row, while Update and
Uninstall buttons share the second row evenly. This makes Configure
more prominent and separates destructive actions to a second row.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(ui): override inline-flex on Configure button to enable full width
The .btn class uses display: inline-flex which prevents w-full from working.
Added inline style to override with display: flex and width: 100% so the
Configure button properly takes the full width of its row.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(ui): use inline styles for plugin action buttons layout
Replace Tailwind classes with explicit inline styles to ensure proper
two-row layout for plugin action buttons. Configure button on first row
at full width, Update and Uninstall sharing second row evenly.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: Handle permission errors when removing plugin directories
- Added _safe_remove_directory() method to handle permission errors gracefully
- Fixes permissions on __pycache__ directories before removal
- Updates uninstall_plugin() and install methods to use safe removal
- Resolves [Errno 13] Permission denied errors during plugin install/uninstall
* feat(wifi): Add grace period for AP mode and improve setup documentation
- Add 90-second grace period (3 checks at 30s intervals) before enabling AP mode
- Change AP to open network (no password) for easier initial setup
- Add verification script for WiFi setup
- Update documentation with grace period details and open network info
- Improve WiFi monitor daemon logging and error handling
* feat(wifi): Add Trixie compatibility and dynamic interface discovery
- Add dynamic WiFi interface discovery instead of hardcoded wlan0
- Supports traditional (wlan0), predictable (wlp2s0), and USB naming
- Falls back gracefully if detection fails
- Add Raspberry Pi OS Trixie (Debian 13) detection and compatibility
- Detect Netplan configuration and connection file locations
- Disable PMF (Protected Management Frames) on Trixie for better
client compatibility with certain WiFi adapters
- Improve nmcli hotspot setup for Trixie
- Add explicit IP configuration (192.168.4.1/24)
- Add channel configuration to hotspot creation
- Handle Trixie's default 10.42.0.1 IP override
- Add dnsmasq conflict detection
- Warn if Pi-hole or other DNS services are using dnsmasq
- Create backup before overwriting config
- Improve error handling
- Replace bare except clauses with specific exceptions
- All subprocess calls now have explicit timeouts
- Document sudoers requirements in module docstring
- List all required NOPASSWD entries for ledpi user
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wifi): Use NM_CONNECTIONS_PATHS constant in _detect_trixie
Replace hardcoded Path instances with references to the
NM_CONNECTIONS_PATHS constant for consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(verify): Use ETH_CONNECTED and AP_ACTIVE in summary output
Add connectivity summary section that displays Ethernet and AP mode
status using the previously unused ETH_CONNECTED and AP_ACTIVE flags.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(cache): Add intelligent disk cache cleanup with retention policies
- Add cleanup_expired_files() method to DiskCache class
- Implement retention policies based on cache data types:
* Odds data: 2 days (lines move frequently)
* Live/recent/leaderboard: 7 days (weekly updates)
* News/stocks: 14 days
* Upcoming/schedules/team_info/logos: 60 days (stable data)
- Add cleanup_disk_cache() orchestration in CacheManager
- Start background cleanup thread running every 24 hours
- Run cleanup on application startup
- Add disk cleanup metrics tracking
- Comprehensive logging with cleanup statistics
This prevents disk cache from accumulating indefinitely while preserving
important season data longer than volatile live game data.
* refactor(cache): improve disk cache cleanup implementation
- Implement force parameter throttle mechanism in cleanup_disk_cache
- Fix TOCTOU race condition in disk cache cleanup (getsize/remove)
- Reduce lock contention by processing files outside lock where possible
- Add CacheStrategyProtocol for better type safety (replaces Any)
- Move time import to module level in cache_metrics
- Defer initial cleanup to background thread for non-blocking startup
- Add graceful shutdown mechanism with threading.Event for cleanup thread
- Add stop_cleanup_thread() method for controlled thread termination
* fix(cache): improve disk cache cleanup initialization and error handling
- Only start cleanup thread when disk caching is enabled (cache_dir is set)
- Remove unused retention policy keys (leaderboard, live_scores, logos)
- Handle FileNotFoundError as benign race condition in cleanup
- Preserve existing OSError handling for actual file system errors
---------
Co-authored-by: Chuck <chuck@example.com>
The basketball scoreboard plugin uses sport_key="ncaam" and "ncaaw",
but LogoDownloader only had "ncaam_basketball" mapped. This caused
get_logo_directory() to fall back to "assets/sports/ncaam_logos"
(non-existent) instead of "assets/sports/ncaa_logos".
Added aliases to both LOGO_DIRECTORIES and API_ENDPOINTS:
- ncaam -> assets/sports/ncaa_logos
- ncaaw -> assets/sports/ncaa_logos
- ncaaw_basketball -> assets/sports/ncaa_logos
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: migrate from submodules to multi-root workspace for plugins
- Updated LEDMatrix.code-workspace to include all plugin repos as root folders
- Removed symlinks from plugin-repos/ and plugins/ directories
- Updated .gitignore to reflect new plugin management approach
- Added setup_plugin_repos.py script for managing plugin symlinks (if needed)
- Added MULTI_ROOT_WORKSPACE_SETUP.md documentation
Plugins are now managed as independent repositories via multi-root workspace,
allowing for easier development and independent updates without modifying
the LEDMatrix project structure.
* Fix MULTI_ROOT_WORKSPACE_SETUP.md and add JSON error handling
- Remove deprecated clone_plugin_repos.py command reference
- Add language tag to directory tree code fence (fixes MD040)
- Add JSONDecodeError handling in setup_plugin_repos.py with user-friendly error messages
---------
Co-authored-by: Chuck <chuck@example.com>
* chore: Update basketball-scoreboard submodule for odds font fix
* fix(plugins): Add missing closing brace in file-upload widget if block
Fixed syntax error where the if statement starting at line 2949 was missing
its closing brace before the else if statement. This caused 'Unexpected token
else' error at line 3257 when parsing the loadCustomHtmlWidget function.
The fix adds the missing closing brace at line 3048 to properly close the
if block before the else if chain continues.
* fix(plugins): Resolve unmatched else if syntax error in plugins_manager.js
- Fixed indentation of else if chain for custom-feeds widget (line 3203)
- Fixed indentation of final else block (line 3240)
- Added missing closing brace to properly close array handling block (line 3257)
- Resolves 'Unexpected token else' syntax error at line 3257
- Allows plugin store to load correctly
* fix: Update plugins_manager.js cache-busting version to force reload of syntax fix
---------
Co-authored-by: Chuck <chuck@example.com>
* chore: Update basketball-scoreboard submodule for odds font fix
* fix(plugins): Add missing closing brace in file-upload widget if block
Fixed syntax error where the if statement starting at line 2949 was missing
its closing brace before the else if statement. This caused 'Unexpected token
else' error at line 3257 when parsing the loadCustomHtmlWidget function.
The fix adds the missing closing brace at line 3048 to properly close the
if block before the else if chain continues.
---------
Co-authored-by: Chuck <chuck@example.com>
* chore: Update basketball-scoreboard submodule for odds font fix
* feat(widgets): Add widget registry system for plugin configuration forms
- Create core widget registry system (registry.js, base-widget.js)
- Extract existing widgets to separate modules:
- file-upload.js: Image upload with drag-and-drop, preview, delete, scheduling
- checkbox-group.js: Multi-select checkboxes for array fields
- custom-feeds.js: Table-based RSS feed editor with logo uploads
- Implement plugin widget loading system (plugin-loader.js)
- Add comprehensive documentation (widget-guide.md, README.md)
- Include example custom widget (example-color-picker.js)
- Maintain backwards compatibility with existing plugins
- All widget handlers available globally for existing functionality
This enables:
- Reusable UI components for plugin configuration forms
- Third-party plugins to create custom widgets without modifying LEDMatrix
- Modular widget architecture for future enhancements
Existing plugins (odds-ticker, static-image, news) continue to work without changes.
* fix(widgets): Security and correctness fixes for widget system
- base-widget.js: Fix escapeHtml to always escape (coerce to string first)
- base-widget.js: Add sanitizeId helper for safe DOM ID usage
- base-widget.js: Use DOM APIs in showError instead of innerHTML
- checkbox-group.js: Normalize types in setValue for consistent comparison
- custom-feeds.js: Implement setValue with full row creation logic
- example-color-picker.js: Validate hex colors before using in style attributes
- file-upload.js: Replace innerHTML with DOM creation to prevent XSS
- file-upload.js: Preserve open schedule editors when updating image list
- file-upload.js: Normalize types when filtering deleted files
- file-upload.js: Sanitize imageId in openImageSchedule and all schedule handlers
- file-upload.js: Fix max-files check order and use allowed_types from config
- README.md: Add security guidance for ID sanitization in examples
* fix(widgets): Additional security and error handling improvements
- scripts/update_plugin_repos.py: Add explicit UTF-8 encoding and proper error handling for file operations
- scripts/update_plugin_repos.py: Fix git fetch/pull error handling with returncode checks and specific exception types
- base-widget.js: Guard notify method against undefined/null type parameter
- file-upload.js: Remove inline handlers from schedule template, use addEventListener with data attributes
- file-upload.js: Update hideUploadProgress to show dynamic file types from config instead of hardcoded list
- README.md: Update Color Picker example to use sanitized fieldId throughout
* fix(widgets): Update Slider example to use sanitized fieldId
- Add sanitizeId helper to Slider example render, getValue, and setValue methods
- Use sanitizedFieldId for all DOM IDs and query selectors
- Maintain consistency with Color Picker example pattern
* fix(plugins_manager): Move configurePlugin and togglePlugin to top of file
- Move configurePlugin and togglePlugin definitions to top level (after uninstallPlugin)
- Ensures these critical functions are available immediately when script loads
- Fixes 'Critical functions not available after 20 attempts' error
- Functions are now defined before any HTML rendering checks
* fix(plugins_manager): Fix checkbox state saving using querySelector
- Add escapeCssSelector helper function for safe CSS selector usage
- Replace form.elements[actualKey] with form.querySelector for boolean fields
- Properly handle checkbox checked state using element.checked property
- Fix both schema-based and schema-less boolean field processing
- Ensures checkboxes with dot notation names (nested fields) work correctly
Fixes issue where checkbox states were not properly saved when field names
use dot notation (e.g., 'display.scroll_enabled'). The form.elements
collection doesn't reliably handle dot notation in bracket notation access.
* fix(base.html): Fix form element lookup for dot notation field names
- Add escapeCssSelector helper function (both as method and standalone)
- Replace form.elements[key] with form.querySelector for element type detection
- Fixes element lookup failures when field names use dot notation
- Ensures checkbox and multi-select skipping logic works correctly
- Applies fix to both Alpine.js method and standalone function
This complements the fix in plugins_manager.js to ensure all form
element lookups handle nested field names (e.g., 'display.scroll_enabled')
reliably across the entire web interface.
* fix(plugins_manager): Add race condition protection to togglePlugin
- Initialize window._pluginToggleRequests map for per-plugin request tokens
- Generate unique token for each toggle request to track in-flight requests
- Disable checkbox and wrapper UI during request to prevent overlapping toggles
- Add visual feedback with opacity and pointer-events-none classes
- Verify token matches before applying response updates (both success and error)
- Ignore out-of-order responses to preserve latest user intent
- Clear token and re-enable UI after request completes
Prevents race conditions when users rapidly toggle plugins, ensuring
only the latest toggle request's response affects the UI state.
* refactor(escapeCssSelector): Use CSS.escape() for better selector safety
- Prefer CSS.escape() when available for proper CSS selector escaping
- Handles edge cases: unicode characters, leading digits, and spec compliance
- Keep regex-based fallback for older browsers without CSS.escape support
- Update all three instances: plugins_manager.js and both in base.html
CSS.escape() is the standard API for escaping CSS selectors and provides
more robust handling than custom regex, especially for unicode and edge cases.
* fix(plugins_manager): Fix syntax error - missing closing brace for file-upload if block
- Add missing closing brace before else-if for checkbox-group widget
- Fixes 'Unexpected token else' error at line 3138
- The if block for file-upload widget (line 3034) was missing its closing brace
- Now properly structured: if (file-upload) { ... } else if (checkbox-group) { ... }
* fix(plugins_manager): Fix indentation in file-upload widget if block
- Properly indent all code inside the file-upload if block
- Fix template string closing brace indentation
- Ensures proper structure: if (file-upload) { ... } else if (checkbox-group) { ... }
- Resolves syntax error at line 3138
* fix(plugins_manager): Skip checkbox-group [] inputs to prevent config leakage
- Add skip logic for keys ending with '[]' in handlePluginConfigSubmit
- Prevents checkbox-group bracket notation inputs from leaking into config
- Checkbox-group widgets emit name="...[]" checkboxes plus a _data JSON field
- The _data field is already processed correctly, so [] inputs are redundant
- Prevents schema validation failures and extra config keys
The checkbox-group widget creates:
1. Individual checkboxes with name="fullKey[]" (now skipped)
2. Hidden input with name="fullKey_data" containing JSON array (processed)
3. Sentinel hidden input with name="fullKey[]" and empty value (now skipped)
* fix(plugins_manager): Normalize string booleans when checkbox input is missing
- Fix boolean field processing to properly normalize string booleans in fallback path
- Prevents "false"/"0" from being coerced to true when checkbox element is missing
- Handles common string boolean representations: 'true', 'false', '1', '0', 'on', 'off'
- Applies to both schema-based (lines 2386-2400) and schema-less (lines 2423-2433) paths
When a checkbox element cannot be found, the fallback logic now:
1. Checks if value is a string and normalizes known boolean representations
2. Treats undefined/null as false
3. Coerces other types to boolean using Boolean()
This ensures string values like "false" or "0" are correctly converted to false
instead of being treated as truthy non-empty strings.
* fix(base.html): Improve escapeCssSelector fallback to match CSS.escape behavior
- Handle leading digits by converting to hex escape (e.g., '1' -> '\0031 ')
- Handle leading whitespace by converting to hex escape (e.g., ' ' -> '\0020 ')
- Escape internal spaces as '\ ' (preserving space in hex escapes)
- Ensures trailing space after hex escapes per CSS spec
- Applies to both Alpine.js method and standalone function
The fallback now better matches CSS.escape() behavior for older browsers:
1. Escapes leading digits (0-9) as hex escapes with trailing space
2. Escapes leading whitespace as hex escapes with trailing space
3. Escapes all special characters as before
4. Escapes internal spaces while preserving hex escape format
This prevents selector injection issues with field names starting with digits
or whitespace, matching the standard CSS.escape() API behavior.
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(plugins): Remove compatible_versions requirement from single plugin install
Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.
* fix(7-segment-clock): Update submodule with separator and spacing fixes
* fix(plugins): Add onchange handlers to existing custom feed inputs
- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)
* Add array-of-objects widget support to web UI
- Add support for rendering arrays of objects in web UI (for custom_feeds)
- Implement add/remove/update functions for array-of-objects widgets
- Support file-upload widgets within array items
- Update form data handling to support array JSON data fields
* Update plugins_manager.js cache-busting version
Update version parameter to force browser to load new JavaScript with array-of-objects widget support.
* Fix: Move array-of-objects detection before file-upload/checkbox checks
Move the array-of-objects widget detection to the top of the array handler so it's checked before file-upload and checkbox-group widgets. This ensures custom_feeds is properly detected as an array of objects.
* Update cache-busting version for array-of-objects fix
* Remove duplicate array-of-objects check
* Update cache version again
* Add array-of-objects widget support to server-side template
Add detection and rendering for array-of-objects in the Jinja2 template (plugin_config.html).
This enables the custom_feeds widget to display properly with name, URL, enabled checkbox, and logo upload fields.
The widget is detected by checking if prop.items.type == 'object' && prop.items.properties,
and is rendered before the file-upload widget check.
* Use window. prefix for array-of-objects JavaScript functions
Explicitly use window.addArrayObjectItem, window.removeArrayObjectItem, etc.
in the template to ensure the functions are accessible from inline event handlers.
Also add safety checks to prevent errors if functions aren't loaded yet.
* Fix duplicate display settings in config
Prevent display settings from being saved at both nested (display.hardware/runtime) and root level. The save_main_config function was processing display fields twice - once correctly in the nested structure, and again in the catch-all section creating root-level duplicates.
Added display_fields to the skip list in the catch-all section to prevent root-level duplicates. All code expects the nested format, so this ensures consistency.
* fix: Recreate one-shot install script with APT permission and non-interactive fixes
Recreate one-shot install script that was deleted, with fixes for:
1. APT permission denied errors on /tmp
2. Non-interactive mode support
Fixes:
1. Fix /tmp permissions before running first_time_install.sh:
- chmod 1777 /tmp to ensure APT can write temp files
- Set TMPDIR=/tmp explicitly
- Preserve TMPDIR when using sudo -E
2. Enable non-interactive mode:
- Pass -y flag or LEDMATRIX_ASSUME_YES=1 to first_time_install.sh
- Prevents read prompt failure at line 242 when run via curl | bash
3. Better error handling:
- Temporarily disable errexit to capture exit code
- Re-enable errexit after capturing
- Added fix_tmp_permissions() function
This resolves the 'Permission denied' errors for APT temp files and the
interactive prompt failure when running via pipe.
* fix(plugins): Restore version and display_modes to required_fields and fix array object data persistence
- Restore 'version' and 'display_modes' to required_fields in store_manager.py manifest validation (both occurrences at lines 839 and 977)
- Fix updateArrayObjectData to merge input fields with existing item data to preserve non-editable properties like logo objects
- Implement handleArrayObjectFileUpload to properly upload files and store metadata in data-file-data attribute
- Implement removeArrayObjectFile to properly remove file metadata and update data structure
- Update renderArrayObjectItem to preserve file data in data-file-data attribute when rendering existing items
* fix(plugins): Remove version from required_fields, keep display_modes required
- Remove 'version' from required_fields in store_manager.py (both occurrences)
- Some existing plugins have version: null or no version field (basketball-scoreboard, odds-ticker)
- All code uses safe accessors (manifest.get('version')), so optional is safe
- Keep 'display_modes' as required - all plugins have it and tests expect it
* fix: Preserve exit codes in retry() and fix null handling in JSON data detection
- Fix retry() function to preserve original command exit code by capturing status immediately after command execution
- Fix JSON data detection to prevent null from overwriting config by checking jsonValue !== null before treating as object
- Both fixes prevent edge cases that could cause incorrect behavior or data corruption
* fix: Resolve merge conflict, fix array-of-objects file upload, and improve retry function
- Remove unresolved merge conflict marker in array rendering (checkbox input attributes)
- Fix array-of-objects file upload selector mismatch by adding id to wrapper element
- Fix index-based preserve corruption by using data-item-data attributes instead of array indices
- Add showNotification guards to prevent errors when notifications aren't available
- Fix retry() function to work with set -Eeuo pipefail by disabling errexit for command execution
* fix: Remove duplicate implementations, fix upload config, and add type coercion
- Remove/guard duplicate updateArrayObjectData, handleArrayObjectFileUpload, and removeArrayObjectFile stub implementations that were overwriting real implementations
- Fix hard-coded plugin ID fallback in renderArrayObjectItem - use null instead of 'ledmatrix-news'
- Fix upload config to use uploadConfig.allowed_types and uploadConfig.max_size_mb from schema instead of hard-coded values
- Store uploadConfig in data-upload-config attribute and read it in handleArrayObjectFileUpload for validation
- Add type coercion to updateArrayObjectData: coerce number inputs to Number, array inputs via JSON.parse with comma-split fallback
* fix: Use event-based element lookup in handleArrayObjectFileUpload
- Change from constructing ID to using event.target.closest('.array-object-item') to find item element
- Query fileUploadContainer from itemEl instead of using constructed ID lookup
- Remove reliance on `${fieldId}_item_${itemIndex}` which breaks after reindexing
- Add response.ok check before calling response.json() to avoid JSON parsing errors on HTTP errors
- Handle non-OK responses with proper error messages (JSON parse with fallback)
* fix: Improve HTML escaping and add pluginId validation for file uploads
- Replace manual single-quote escaping with escapeAttribute() for proper HTML escaping in array-of-objects hidden input
- Update default allowed_types to include 'image/jpg' in handleArrayObjectFileUpload
- Add explicit pluginId validation before upload to fail fast with clear error message
- Prevents XSS vulnerabilities and backend rejections from invalid uploads
* fix: Use propKey-scoped selector and harden pluginId validation
- Narrow file widget lookup to use propKey-specific selector (.file-upload-widget-inline[data-prop-key]) to target correct widget when item has multiple file widgets
- Harden pluginId validation by checking typeof pluginId === 'string' before calling trim() to prevent errors on non-string values
---------
Co-authored-by: Chuck <chuck@example.com>
* fix: Use plugin.modes instead of manifest.json for available modes
- Display controller now checks plugin_instance.modes first before falling back to manifest
- This allows plugins to dynamically provide modes based on enabled leagues
- Fixes issue where disabled leagues (WNBA, NCAAW) appeared in available modes
- Plugins can now control their available modes at runtime based on config
* fix: Handle permission errors when removing plugin directories
- Added _safe_remove_directory() method to handle permission errors gracefully
- Fixes permissions on __pycache__ directories before removal
- Updates uninstall_plugin() and install methods to use safe removal
- Resolves [Errno 13] Permission denied errors during plugin install/uninstall
* debug(display): Change FPS check logging from debug to info level
- Change FPS check log from DEBUG to INFO to help diagnose scrolling FPS issues
- Add active_mode to log message for clarity
- Helps identify if plugins are being detected for high-FPS mode
* debug(display): Add logging for display_interval in both FPS loops
- Log display_interval when entering high-FPS and normal loops
- Shows expected FPS for high-FPS mode
- Helps diagnose why news ticker shows 50 FPS despite high-FPS detection
* feat: Update soccer-scoreboard submodule with scroll display support
- Submodule now includes full feature parity with football-scoreboard
- Granular display modes for 8 leagues (24 total modes)
- Scroll display mode with game_renderer.py and scroll_display.py
- League registry system with enabled state filtering
- Modernized config_schema.json with per-league scroll settings
- League-aware logo caching to prevent collisions
- Pillow 8.x compatibility for image resampling
Submodule branch: feature/football-feature-parity
Commit: e22a16d
* style(web): Update plugin button colors and reorganize documentation
- Change update button color to yellow-600 in installed plugins section to match plugin config page
- Change refresh plugins button color to blue-600 to match restart display button
- Move DEVELOPMENT.md and MIGRATION_GUIDE.md from root to docs/ directory
- Remove IMPACT_EXPLANATION.md and MERGE_CONFLICT_RESOLUTION_PLAN.md
---------
Co-authored-by: Chuck <chuck@example.com>
* fix: Use plugin.modes instead of manifest.json for available modes
- Display controller now checks plugin_instance.modes first before falling back to manifest
- This allows plugins to dynamically provide modes based on enabled leagues
- Fixes issue where disabled leagues (WNBA, NCAAW) appeared in available modes
- Plugins can now control their available modes at runtime based on config
* fix: Handle permission errors when removing plugin directories
- Added _safe_remove_directory() method to handle permission errors gracefully
- Fixes permissions on __pycache__ directories before removal
- Updates uninstall_plugin() and install methods to use safe removal
- Resolves [Errno 13] Permission denied errors during plugin install/uninstall
* debug(display): Change FPS check logging from debug to info level
- Change FPS check log from DEBUG to INFO to help diagnose scrolling FPS issues
- Add active_mode to log message for clarity
- Helps identify if plugins are being detected for high-FPS mode
* debug(display): Add logging for display_interval in both FPS loops
- Log display_interval when entering high-FPS and normal loops
- Shows expected FPS for high-FPS mode
- Helps diagnose why news ticker shows 50 FPS despite high-FPS detection
---------
Co-authored-by: Chuck <chuck@example.com>
* fix: Use plugin.modes instead of manifest.json for available modes
- Display controller now checks plugin_instance.modes first before falling back to manifest
- This allows plugins to dynamically provide modes based on enabled leagues
- Fixes issue where disabled leagues (WNBA, NCAAW) appeared in available modes
- Plugins can now control their available modes at runtime based on config
* fix: Handle permission errors when removing plugin directories
- Added _safe_remove_directory() method to handle permission errors gracefully
- Fixes permissions on __pycache__ directories before removal
- Updates uninstall_plugin() and install methods to use safe removal
- Resolves [Errno 13] Permission denied errors during plugin install/uninstall
* refactor: Improve error handling in _safe_remove_directory
- Rename unused 'dirs' variable to '_dirs' to indicate intentional non-use
- Use logger.exception() instead of logger.error() to preserve stack traces
- Add comment explaining 0o777 permissions are acceptable (temporary before deletion)
* fix(install): Fix one-shot-install script reliability issues
- Install git and curl before attempting repository clone
- Add HOME variable validation to prevent path errors
- Improve git branch detection (try current branch, main, then master)
- Add validation for all directory change operations
- Improve hostname command handling in success message
- Fix edge cases for better installation success rate
* fix(install): Fix IP address display in installation completion message
- Replace unreliable pipe-to-while-read loop with direct for loop
- Filter out loopback addresses (127.0.0.1, ::1) from display
- Add proper message when no non-loopback IPs are found
- Fixes blank IP address display issue at end of installation
* fix(install): Prevent unintended merges in one-shot-install git pull logic
- Use git pull --ff-only for current branch to avoid unintended merges
- Use git fetch (not pull) for other branches to check existence without merging
- Only update current branch if fast-forward is possible
- Provide better warnings when branch updates fail but other branches exist
- Prevents risk of merging remote main/master into unrelated working branches
* fix(install): Improve IPv6 address handling in installation scripts
- Filter out IPv6 link-local addresses (fe80:) in addition to loopback
- Properly format IPv6 addresses with brackets in URLs (http://[::1]:5000)
- Filter loopback and link-local addresses when selecting IP for display
- Prevents invalid IPv6 URLs and excludes non-useful addresses
- Fixes: first_time_install.sh and one-shot-install.sh IP display logic
* fix: Fix checkbox-group saving and improve dynamic duration calculation
- Fix checkbox-group widget saving by setting values directly in plugin_config
- Fix element_gap calculation bug in ScrollHelper (was over-calculating width)
- Use actual image width instead of calculated width for scroll calculations
- Add comprehensive INFO-level logging for dynamic duration troubleshooting
- Enhanced scroll completion logging with position and percentage details
This fixes issues where checkbox-group values weren't saving correctly
and improves dynamic duration calculation accuracy for scrolling content.
---------
Co-authored-by: Chuck <chuck@example.com>
* fix: Use plugin.modes instead of manifest.json for available modes
- Display controller now checks plugin_instance.modes first before falling back to manifest
- This allows plugins to dynamically provide modes based on enabled leagues
- Fixes issue where disabled leagues (WNBA, NCAAW) appeared in available modes
- Plugins can now control their available modes at runtime based on config
* fix: Handle permission errors when removing plugin directories
- Added _safe_remove_directory() method to handle permission errors gracefully
- Fixes permissions on __pycache__ directories before removal
- Updates uninstall_plugin() and install methods to use safe removal
- Resolves [Errno 13] Permission denied errors during plugin install/uninstall
* refactor: Improve error handling in _safe_remove_directory
- Rename unused 'dirs' variable to '_dirs' to indicate intentional non-use
- Use logger.exception() instead of logger.error() to preserve stack traces
- Add comment explaining 0o777 permissions are acceptable (temporary before deletion)
* fix(install): Fix one-shot-install script reliability issues
- Install git and curl before attempting repository clone
- Add HOME variable validation to prevent path errors
- Improve git branch detection (try current branch, main, then master)
- Add validation for all directory change operations
- Improve hostname command handling in success message
- Fix edge cases for better installation success rate
* fix(install): Fix IP address display in installation completion message
- Replace unreliable pipe-to-while-read loop with direct for loop
- Filter out loopback addresses (127.0.0.1, ::1) from display
- Add proper message when no non-loopback IPs are found
- Fixes blank IP address display issue at end of installation
* fix(install): Prevent unintended merges in one-shot-install git pull logic
- Use git pull --ff-only for current branch to avoid unintended merges
- Use git fetch (not pull) for other branches to check existence without merging
- Only update current branch if fast-forward is possible
- Provide better warnings when branch updates fail but other branches exist
- Prevents risk of merging remote main/master into unrelated working branches
* fix(install): Improve IPv6 address handling in installation scripts
- Filter out IPv6 link-local addresses (fe80:) in addition to loopback
- Properly format IPv6 addresses with brackets in URLs (http://[::1]:5000)
- Filter loopback and link-local addresses when selecting IP for display
- Prevents invalid IPv6 URLs and excludes non-useful addresses
- Fixes: first_time_install.sh and one-shot-install.sh IP display logic
---------
Co-authored-by: Chuck <chuck@example.com>
* fix: Use plugin.modes instead of manifest.json for available modes
- Display controller now checks plugin_instance.modes first before falling back to manifest
- This allows plugins to dynamically provide modes based on enabled leagues
- Fixes issue where disabled leagues (WNBA, NCAAW) appeared in available modes
- Plugins can now control their available modes at runtime based on config
* fix: Handle permission errors when removing plugin directories
- Added _safe_remove_directory() method to handle permission errors gracefully
- Fixes permissions on __pycache__ directories before removal
- Updates uninstall_plugin() and install methods to use safe removal
- Resolves [Errno 13] Permission denied errors during plugin install/uninstall
* refactor: Improve error handling in _safe_remove_directory
- Rename unused 'dirs' variable to '_dirs' to indicate intentional non-use
- Use logger.exception() instead of logger.error() to preserve stack traces
- Add comment explaining 0o777 permissions are acceptable (temporary before deletion)
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(plugins): Remove compatible_versions requirement from single plugin install
Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.
* fix(7-segment-clock): Update submodule with separator and spacing fixes
* fix(plugins): Add onchange handlers to existing custom feed inputs
- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)
* Add array-of-objects widget support to web UI
- Add support for rendering arrays of objects in web UI (for custom_feeds)
- Implement add/remove/update functions for array-of-objects widgets
- Support file-upload widgets within array items
- Update form data handling to support array JSON data fields
* Update plugins_manager.js cache-busting version
Update version parameter to force browser to load new JavaScript with array-of-objects widget support.
* Fix: Move array-of-objects detection before file-upload/checkbox checks
Move the array-of-objects widget detection to the top of the array handler so it's checked before file-upload and checkbox-group widgets. This ensures custom_feeds is properly detected as an array of objects.
* Update cache-busting version for array-of-objects fix
* Remove duplicate array-of-objects check
* Update cache version again
* Add array-of-objects widget support to server-side template
Add detection and rendering for array-of-objects in the Jinja2 template (plugin_config.html).
This enables the custom_feeds widget to display properly with name, URL, enabled checkbox, and logo upload fields.
The widget is detected by checking if prop.items.type == 'object' && prop.items.properties,
and is rendered before the file-upload widget check.
* Use window. prefix for array-of-objects JavaScript functions
Explicitly use window.addArrayObjectItem, window.removeArrayObjectItem, etc.
in the template to ensure the functions are accessible from inline event handlers.
Also add safety checks to prevent errors if functions aren't loaded yet.
* Fix syntax error: Missing indentation for html += in array else block
The html += statement was outside the else block, causing a syntax error.
Fixed by properly indenting it inside the else block.
* Update cache version for syntax fix
* Add debug logging to diagnose addArrayObjectItem availability
* Fix: Wrap array-of-objects functions in window check and move outside IIFE
Ensure functions are available globally by wrapping them in a window check
and ensuring they're defined outside any IIFE scope. Also fix internal
function calls to use window.updateArrayObjectData for consistency.
* Update cache version for array-of-objects fix
* Move array-of-objects functions outside IIFE to make them globally available
The functions were inside the IIFE scope, making them inaccessible from
inline event handlers. Moving them outside the IIFE ensures they're
available on window when the script loads.
* Update cache version for IIFE fix
* Fix: Add array-of-objects functions after IIFE ends
The functions were removed from inside the IIFE but never added after it.
Also removed orphaned code that was causing syntax errors.
* Update cache version for array-of-objects fix
* Fix: Remove all orphaned code and properly add array-of-objects functions after IIFE
* Add array-of-objects functions after IIFE ends
These functions must be outside the IIFE to be accessible from inline
event handlers in the server-rendered template.
* Update cache version for syntax fix
* Fix syntax error: Add missing closing brace for else block
* Update cache version for syntax fix
* Replace complex array-of-objects widget with simple table interface
- Replace nested array-of-objects widget with clean table interface
- Table shows: Name, URL, Logo (with upload), Enabled checkbox, Delete button
- Fix file-upload widget detection order to prevent breaking static-image plugin
- Add simple JavaScript functions for add/remove rows and logo upload
- Much more intuitive and easier to use
* Add simple table interface for custom feeds
- Replace complex array-of-objects widget with clean table
- Table columns: Name, URL, Logo (upload), Enabled checkbox, Delete
- Use dot notation for form field names (feeds.custom_feeds.0.name)
- Add JavaScript functions for add/remove rows and logo upload
- Fix file-upload detection order to prevent breaking static-image plugin
* Fix custom feeds table issues
- Fix JavaScript error in removeCustomFeedRow (get tbody before removing row)
- Improve array conversion logic to handle nested paths like feeds.custom_feeds
- Add better error handling and debug logging for array conversion
- Ensure dicts with numeric keys are properly converted to arrays before validation
* Add fallback fix for feeds.custom_feeds dict-to-array conversion
- Add explicit fallback conversion for feeds.custom_feeds if fix_array_structures misses it
- This ensures the dict with numeric keys is converted to an array before validation
- Logo field is already optional in schema (not in required array)
* feat(web): Add checkbox-group widget support for plugin config arrays
Add server-side rendering support for checkbox-group widget in plugin
configuration forms. This allows plugins to use checkboxes for multi-select
array fields instead of comma-separated text inputs.
The implementation:
- Checks for x-widget: 'checkbox-group' in schema
- Renders checkboxes for each enum item in items.enum
- Supports custom labels via x-options.labels
- Works with any plugin that follows the pattern
Already used by:
- ledmatrix-news plugin (enabled_feeds)
- odds-ticker plugin (enabled_leagues)
* feat(install): Add one-shot installation script
- Create comprehensive one-shot installer with robust error handling
- Includes network checks, disk space validation, and retry logic
- Handles existing installations gracefully (idempotent)
- Updates README with quick install command prominently featured
- Manual installation instructions moved to collapsible section
The script provides explicit error messages and never fails silently.
All prerequisites are validated before starting installation.
* fix: Remove accidental plugins/7-segment-clock submodule entry
Remove uninitialized submodule 'plugins/7-segment-clock' that was
accidentally included. This submodule is not related to the one-shot
installer feature and should not be part of this PR.
- Remove submodule entry from .gitmodules
- Remove submodule from git index
- Clean up submodule configuration
* fix(array-objects): Fix schema lookup, reindexing, and disable file upload
Address PR review feedback for array-of-objects helpers:
1. Schema resolution: Use getSchemaProperty() instead of manual traversal
- Fixes nested array-of-objects schema lookup (e.g., news.custom_feeds)
- Now properly descends through .properties for nested objects
2. Reindexing: Replace brittle regex with targeted patterns
- Only replace index in bracket notation [0], [1], etc. for names
- Only replace _item_<digits> pattern for IDs (not arbitrary digits)
- Use specific function parameter patterns for onclick handlers
- Prevents corruption of fieldId, pluginId, or other numeric values
3. File upload: Disable widget until properly implemented
- Hide/disable upload button with clear message
- Show existing logos if present but disable upload functionality
- Prevents silent failures when users attempt to upload files
- Added TODO comments for future implementation
Also fixes exit code handling in one-shot-install.sh to properly capture
first_time_install.sh exit status before error trap fires.
* fix(security): Fix XSS vulnerability in handleCustomFeedLogoUpload
Replace innerHTML usage with safe DOM manipulation using createElement
and setAttribute to prevent XSS when injecting uploadedFile.path and
uploadedFile.id values.
- Clear logoCell using textContent instead of innerHTML
- Create all DOM elements using document.createElement
- Set uploadedFile.path and uploadedFile.id via setAttribute (automatically escaped)
- Properly structure DOM tree by appending elements in order
- Prevents malicious HTML/script injection through file path or ID values
* fix: Update upload button onclick when reindexing custom feed rows
Fix removeCustomFeedRow to update button onclick handlers that reference
file input IDs with _logo_<index> when rows are reindexed after deletion.
Previously, after deleting a row, the upload button's onclick still referenced
the old file input ID, causing the upload functionality to fail.
Now properly updates:
- getElementById('..._logo_<num>') patterns in onclick handlers
- Other _logo_<num> patterns in button onclick strings
- Function parameter indices in onclick handlers
This ensures upload buttons continue to work correctly after row deletion.
* fix: Make custom feeds table widget-specific instead of generic fallback
Replace generic array-of-objects check with widget-specific check for
'custom-feeds' widget to prevent hardcoded schema from breaking other
plugins with different array-of-objects structures.
Changes:
- Check for x-widget == 'custom-feeds' before rendering custom feeds table
- Add schema validation to ensure required fields (name, url) exist
- Show warning message if schema doesn't match expected structure
- Fall back to generic array input for other array-of-objects schemas
- Add comments for future generic array-of-objects support
This ensures the hardcoded custom feeds table (name, url, logo, enabled)
only renders when explicitly requested via widget type, preventing
breakage for other plugins with different array-of-objects schemas.
* fix: Add image/gif to custom feed logo upload accept attribute
Update file input accept attributes for custom feed logo uploads to include
image/gif, making it consistent with the file-upload widget which also
allows GIF images.
Updated in three places:
- Template file input (plugin_config.html)
- JavaScript addCustomFeedRow function (base.html)
- Dynamic file input creation in handleCustomFeedLogoUpload (base.html)
All custom feed logo upload inputs now accept: image/png, image/jpeg,
image/bmp, image/gif
* fix: Add hidden input for enabled checkbox to ensure false is submitted
Add hidden input with value='false' before enabled checkbox in custom feeds
table to ensure an explicit false value is sent when checkbox is unchecked.
Pattern implemented:
- Hidden input: name='enabled', value='false' (always submitted)
- Checkbox: name='enabled', value='true' (only submitted when checked)
- When unchecked: only hidden input submits (false)
- When checked: both submit, checkbox value (true) overwrites hidden
Updated in two places:
- Template checkbox in plugin_config.html (existing rows)
- JavaScript addCustomFeedRow function in base.html (new rows)
Backend verification:
- Backend (api_v3.py) handles string boolean values and converts properly
- JavaScript form processing explicitly checks element.checked, independent of this pattern
- Standard form submission uses last value when multiple values share same name
* fix: Expose renderArrayObjectItem to window for addArrayObjectItem
Fix scope issue where renderArrayObjectItem is defined inside IIFE but
window.addArrayObjectItem is defined outside, causing the function check
to always fail and fallback to degraded HTML rendering.
Problem:
- renderArrayObjectItem (line 2469) is inside IIFE (lines 796-6417)
- window.addArrayObjectItem (line 6422) is outside IIFE
- Check 'typeof renderArrayObjectItem === function' at line 6454 always fails
- Fallback code lacks file upload widgets, URL input types, descriptions, styling
Solution:
- Expose renderArrayObjectItem to window object before IIFE closes
- Function maintains closure access to escapeHtml and other IIFE-scoped functions
- Newly added items now have full functionality matching initially rendered items
* fix: Reorder array type checks to match template order
Fix inconsistent rendering where JavaScript and Jinja template had opposite
ordering for array type checks, causing schemas with both x-widget: file-upload
AND items.type: object (like static-image) to render differently.
Problem:
- Template checks file-upload FIRST (to avoid breaking static-image plugin)
- JavaScript checked array-of-objects FIRST
- Server-rendered forms showed file-upload widget correctly
- JS-rendered forms incorrectly displayed array-of-objects table widget
Solution:
- Reorder JavaScript checks to match template order:
1. Check file-upload widget FIRST
2. Check checkbox-group widget
3. Check custom-feeds widget
4. Check array-of-objects as fallback
5. Regular array input (comma-separated)
This ensures consistent rendering between server-rendered and JS-rendered forms
for schemas that have both x-widget: file-upload AND items.type: object.
* fix: Handle None value for feeds config to prevent TypeError
Fix crash when plugin_config['feeds'] exists but is None, causing
TypeError when checking 'custom_feeds' in feeds_config.
Problem:
- When plugin_config['feeds'] exists but is None, dict.get('feeds', {})
returns None (not the default {}) because dict.get() only uses default
when key doesn't exist, not when value is None
- Line 3642's 'custom_feeds' in feeds_config raises TypeError because
None is not iterable
- This can crash the API endpoint if a plugin config has feeds: null
Solution:
- Change plugin_config.get('feeds', {}) to plugin_config.get('feeds') or {}
to ensure feeds_config is always a dict (never None)
- Add feeds_config check before 'in' operator for extra safety
This ensures the code gracefully handles feeds: null in plugin configuration.
* fix: Add default value for AVAILABLE_SPACE to prevent TypeError
Fix crash when df produces unexpected output that results in empty
AVAILABLE_SPACE variable, causing 'integer expression expected' error.
Problem:
- df may produce unexpected output format (different locale, unusual
filesystem name spanning lines, or non-standard df implementation)
- While '|| echo "0"' handles pipeline failures, it doesn't trigger if
awk succeeds but produces no output (empty string)
- When AVAILABLE_SPACE is empty, comparison [ "$AVAILABLE_SPACE" -lt 500 ]
fails with 'integer expression expected' error
- With set -e, this causes script to exit unexpectedly
Solution:
- Add AVAILABLE_SPACE=${AVAILABLE_SPACE:-0} before comparison to ensure
variable always has a numeric value (defaults to 0 if empty)
- This gracefully handles edge cases where df/awk produces unexpected output
* fix: Wrap debug console.log in debug flag check
Fix unconditional debug logging that outputs internal implementation
details to browser console for all users.
Problem:
- console.log('[ARRAY-OBJECTS] Functions defined on window:', ...)
executes unconditionally when page loads
- Outputs debug information about function availability to all users
- Appears to be development/debugging code inadvertently included
- Noisy console output in production
Solution:
- Wrap console.log statement in _PLUGIN_DEBUG_EARLY check to only
output when pluginDebug localStorage flag is enabled
- Matches pattern used elsewhere in the file for debug logging
- Debug info now only visible when explicitly enabled via
localStorage.setItem('pluginDebug', 'true')
* fix: Expose getSchemaProperty, disable upload widget, handle bracket notation arrays
Multiple fixes for array-of-objects and form processing:
1. Expose getSchemaProperty to window (plugins_manager.js):
- getSchemaProperty was defined inside IIFE but needed by global functions
- Added window.getSchemaProperty = getSchemaProperty before IIFE closes
- Updated window.addArrayObjectItem to use window.getSchemaProperty
- Fixes ReferenceError when dynamically adding array items
2. Disable upload widget for custom feeds (plugin_config.html):
- File input and Upload button were still active but should be disabled
- Removed onchange/onclick handlers, added disabled and aria-disabled
- Added visible disabled styling and tooltip
- Existing logos continue to display but uploads are prevented
- Matches PR objectives to disable upload until fully implemented
3. Handle bracket notation array fields (api_v3.py):
- checkbox-group uses name="field_name[]" which sends multiple values
- request.form.to_dict() collapses duplicate keys (only keeps last value)
- Added handling to detect fields ending with "[]" before to_dict()
- Use request.form.getlist() to get all values, combine as comma-separated
- Processed before existing array index field handling
- Fixes checkbox-group losing all but last selected value
* fix: Remove duplicate submit handler to prevent double POSTs
Remove document-level submit listener that conflicts with handlePluginConfigSubmit,
causing duplicate form submissions with divergent payloads.
Problem:
- handlePluginConfigSubmit correctly parses JSON from _data fields and maps to
flatConfig[baseKey] for patternProperties and array-of-objects
- Document-level listener (line 5368) builds its own config without understanding
_data convention and posts independently via savePluginConfiguration
- Every submit now sends two POSTs with divergent payloads:
- First POST: Correct structure with parsed _data fields
- Second POST: Incorrect structure with raw _data fields, missing structure
- Arrays-of-objects and patternProperties saved incorrectly in second request
Solution:
- Remove document-level submit listener for #plugin-config-form
- Rely solely on handlePluginConfigSubmit which is already attached to the form
- handlePluginConfigSubmit properly handles all form-to-config conversion including:
- _data field parsing (JSON from hidden fields)
- Type-aware conversion using schema
- Dot notation to nested object conversion
- PatternProperties and array-of-objects support
Note: savePluginConfiguration function remains for use by JSON editor saves
* fix: Use indexed names for checkbox-group to work with existing parser
Change checkbox-group widget to use indexed field names instead of bracket
notation, so the existing indexed field parser correctly handles multiple
selected values.
Problem:
- checkbox-group uses name="{{ full_key }}[]" which requires bracket
notation handling in backend
- While bracket notation handler exists, using indexed names is more robust
and leverages existing well-tested indexed field parser
- Indexed field parser already handles fields like "field_name.0",
"field_name.1" correctly
Solution:
- Template: Change name="{{ full_key }}[]" to name="{{ full_key }}.{{
loop.index0 }}"
- JavaScript: Update checkbox-group rendering to use name="."
- Backend indexed field parser (lines 3364-3388) already handles this pattern:
- Detects fields ending with numeric indices (e.g., ".0", ".1")
- Groups them by base_path and sorts by index
- Combines into array correctly
This ensures checkbox-group values are properly preserved when multiple
options are selected, working with the existing schema-based parsing system.
* fix: Set values from item data in fallback array-of-objects rendering
Fix fallback code path for rendering array-of-objects items to properly
set input values from existing item data, matching behavior of proper
renderArrayObjectItem function.
Problem:
- Fallback code at lines 3078-3091 and 6471-6486 creates input elements
without setting values from existing item data
- Text inputs have no value attribute set
- Checkboxes have no checked attribute computed from item properties
- Users would see empty form fields instead of existing configuration data
- Proper renderArrayObjectItem function correctly sets values (line 2556)
Solution:
- Extract propValue from item data: item[propKey] with schema default fallback
- For text inputs: Set value attribute with HTML-escaped propValue
- For checkboxes: Set checked attribute based on propValue truthiness
- Add inline HTML escaping for XSS prevention (since fallback code may
run outside IIFE scope where escapeHtml function may not be available)
This ensures fallback rendering displays existing data correctly when
window.renderArrayObjectItem is not available.
* fix: Remove extra closing brace breaking if/else chain
Remove stray closing brace at line 3127 that was breaking the if/else chain
before the 'else if (prop.enum)' branch, causing 'Unexpected token else'
syntax error.
Problem:
- Extra '}' at line 3127 closed the prop.type === 'array' block prematurely
- This broke the if/else chain, causing syntax error when parser reached
'else if (prop.enum)' at line 3128
- Structure was: } else if (array) { ... } } } else if (enum) - extra brace
Solution:
- Removed the extra closing brace at line 3127
- Structure now correctly: } else if (array) { ... } } else if (enum)
- Verified with Node.js syntax checker - no errors
* fix: Remove local logger assignments to prevent UnboundLocalError
Remove all local logger assignments inside save_plugin_config function that
were shadowing the module-level logger, causing UnboundLocalError when nested
helpers like normalize_config_values() or debug checks reference logger before
those assignments run.
Problem:
- Module-level logger exists at line 13: logger = logging.getLogger(__name__)
- Multiple local assignments inside save_plugin_config (lines 3361, 3401, 3421,
3540, 3660, 3977, 4093, 4118) make logger a local variable for entire function
- Python treats logger as local for entire function scope when any assignment
exists, causing UnboundLocalError if logger is used before assignments
- Nested helpers like normalize_config_values() or debug checks that reference
logger before local assignments would fail
Solution:
- Removed all local logger = logging.getLogger(__name__) assignments in
save_plugin_config function
- Use module-level logger directly throughout the function
- Removed redundant import logging statements that were only used for logger
- This ensures logger is always available and references the module-level logger
All logger references now use the module-level logger without shadowing.
* fix: Fix checkbox-group serialization and array-of-objects key leakage
Multiple fixes for array-of-objects and checkbox-group widgets:
1. Fix checkbox-group serialization (JS and template):
- Changed from indexed names (categories.0, categories.1) to _data pattern
- Added updateCheckboxGroupData() function to sync selected values
- Hidden input stores JSON array of selected enum values
- Checkboxes use data-checkbox-group and data-option-value attributes
- Fixes issue where config.categories became {0: true, 1: true} instead of ['nfl', 'nba']
- Now correctly serializes to array using existing _data handling logic
2. Prevent array-of-objects per-item key leakage:
- Added skip pattern in handlePluginConfigSubmit for _item_<n>_ names
- Removed name attributes from per-item inputs in renderArrayObjectItem
- Per-item inputs now rely solely on hidden _data field
- Prevents feeds_item_0_name from leaking into flatConfig
3. Add type coercion to updateArrayObjectData:
- Consults itemsSchema.properties[propKey].type for coercion
- Handles integer and number types correctly
- Preserves string values as-is
- Ensures numeric fields in array items are stored as numbers
4. Ensure currentPluginConfig is always available:
- Updated addArrayObjectItem to check window.currentPluginConfig first
- Added error logging if schema not available
- Prevents ReferenceError when global helpers need schema
This ensures checkbox-group arrays serialize correctly and array-of-objects
per-item fields don't leak extra keys into the configuration.
* fix: Make _data field matching more specific to prevent false positives
Fix overly broad condition that matched any field containing '_data',
causing false positives and inconsistent key transformation.
Problem:
- Condition 'key.endsWith('_data') || key.includes('_data')' matches any
field containing '_data' anywhere (e.g., 'meta_data_field', 'custom_data_config')
- key.replace(/_data$/, '') only removes '_data' from end, making logic inconsistent
- Fields with '_data' in middle get matched but key isn't transformed
- If their value happens to be valid JSON, it gets incorrectly parsed
Solution:
- Remove 'key.includes('_data')' clause
- Only check 'key.endsWith('_data')' to match actual _data suffix pattern
- Ensures consistent matching: only fields ending with '_data' are treated
as JSON data fields, and only those get the suffix removed
- Prevents false positives on fields like 'meta_data_field' that happen to
contain '_data' in their name
* fix: Add HTML escaping to prevent XSS in fallback code and checkbox-group
Add proper HTML escaping for schema-derived values to prevent XSS vulnerabilities
in fallback rendering code and checkbox-group widget.
Problem:
- Fallback code in generateFieldHtml (line 3094) doesn't escape propLabel
when building HTML strings, while main renderArrayObjectItem uses escapeHtml()
- Checkbox-group widget (lines 3012-3025) doesn't escape option or label values
- While risk is limited (values come from plugin schemas), malicious plugin
schemas or untrusted schema sources could inject XSS
- Inconsistent with main renderArrayObjectItem which properly escapes
Solution:
- Added escapeHtml() calls for propLabel in fallback array-of-objects rendering
(both locations: generateFieldHtml and addArrayObjectItem fallback)
- Added escapeHtml() calls for option values in checkbox-group widget:
- checkboxId (contains option)
- data-option-value attribute
- value attribute
- label text in span
- Ensures consistent XSS protection across all rendering paths
This prevents potential XSS if plugin schemas contain malicious HTML/script
content in enum values or property titles.
* fix: Recreate one-shot install script with APT permission and non-interactive fixes
Recreate one-shot install script that was deleted, with fixes for:
1. APT permission denied errors on /tmp
2. Non-interactive mode support
Fixes:
1. Fix /tmp permissions before running first_time_install.sh:
- chmod 1777 /tmp to ensure APT can write temp files
- Set TMPDIR=/tmp explicitly
- Preserve TMPDIR when using sudo -E
2. Enable non-interactive mode:
- Pass -y flag or LEDMATRIX_ASSUME_YES=1 to first_time_install.sh
- Prevents read prompt failure at line 242 when run via curl | bash
3. Better error handling:
- Temporarily disable errexit to capture exit code
- Re-enable errexit after capturing
- Added fix_tmp_permissions() function
This resolves the 'Permission denied' errors for APT temp files and the
interactive prompt failure when running via pipe.
* fix: Pass both -y flag and env var to first_time_install.sh for non-interactive mode
Ensure first_time_install.sh runs in non-interactive mode by passing both:
1. The -y command-line flag
2. The LEDMATRIX_ASSUME_YES=1 environment variable
This is necessary because first_time_install.sh re-executes itself with sudo
if not running as root (line 131), and we need to ensure the non-interactive
flag is preserved through the re-execution.
Also added debug_install.sh diagnostic script to help troubleshoot
installation failures on the Pi.
* fix: Improve /tmp permission handling and non-interactive mode detection
Improve handling of /tmp permissions and non-interactive mode:
1. /tmp permissions fix:
- Check current permissions before attempting to fix
- Display warning when fixing incorrect permissions (2775 -> 1777)
- Verify /tmp has permissions 1777 (sticky bit + world writable)
2. Non-interactive mode detection:
- Redirect stdin from /dev/null when running via sudo to prevent
read commands from hanging when stdin is not a TTY
- Add better error message in first_time_install.sh when non-interactive
mode is detected but ASSUME_YES is not set
- Check if stdin is a TTY before attempting interactive read
This fixes the issues identified in diagnostic output:
- /tmp permissions 2775 causing APT write failures
- read -p failing when stdin is not a TTY (curl | bash)
Fixes installation failures when running one-shot install via curl | bash.
* refactor: Simplify /tmp permission handling - only fix if actually wrong
Simplify /tmp permission handling:
- Only check and fix /tmp permissions if they're actually incorrect (not preemptively)
- Remove redundant fix_tmp_permissions() call from prerequisites check
- Keep the fix inline where first_time_install.sh is executed
- When running manually, /tmp usually has correct permissions (1777) so no fix needed
This makes the script less aggressive and avoids unnecessary permission changes
when running manually, while still fixing the issue in automated scenarios.
* fix: Remove user confirmation prompts in install_wifi_monitor.sh for non-interactive mode
Make install_wifi_monitor.sh respect non-interactive mode:
1. Package installation prompt (line 48):
- Check for ASSUME_YES or LEDMATRIX_ASSUME_YES environment variable
- If set, automatically install required packages without prompting
- If stdin is not a TTY (non-interactive), also auto-install packages
- Only prompt user in true interactive mode (TTY available)
2. Continue installation prompt (line 145):
- Already checks for ASSUME_YES, but now also checks LEDMATRIX_ASSUME_YES
- Skip prompt if stdin is not a TTY
- Proceed automatically in non-interactive mode
This fixes installation failures at step 8.5 when running via one-shot
installer or with -y flag, as the script was hanging on user prompts.
* fix: Explicitly pass ASSUME_YES to install_wifi_monitor.sh and simplify package installation
Fix WiFi monitor installation failing at step 8.5:
1. Explicitly pass ASSUME_YES environment variable when calling
install_wifi_monitor.sh from first_time_install.sh to ensure
non-interactive mode is respected
2. Simplify package installation logic in install_wifi_monitor.sh:
- Use apt directly when running as root (from first_time_install.sh)
- Use sudo when running as regular user (direct script execution)
- Always install packages automatically in non-interactive mode
- Only prompt in true interactive mode (TTY available and ASSUME_YES not set)
This ensures packages are installed automatically when running via
one-shot installer or with -y flag, preventing installation failures
at step 8.5.
* refactor: Remove all prompts from install_wifi_monitor.sh - install packages automatically
Simplify WiFi monitor installation by removing all user prompts:
1. Package installation: Always install required packages automatically
- No prompt for missing packages (hostapd, dnsmasq, network-manager)
- Just install them if missing
2. Network connection warning: Remove prompt to continue
- Just display informational message and proceed
- WiFi monitor will handle AP mode automatically if no network
3. Remove ASSUME_YES environment variable passing from first_time_install.sh
- No longer needed since script has no prompts
This makes the installation completely non-interactive and simpler,
preventing any hangs or failures at step 8.5.
* fix: Address multiple issues in debug script, array rendering, and custom feeds
1. debug_install.sh: Make log path dynamic instead of hardcoded
- Compute project root from script location
- Use dynamic LOG_DIR instead of hardcoded /home/ledpi/LEDMatrix/logs/
- Works from any clone location and user
2. plugins_manager.js renderArrayObjectItem: Fix XSS and metadata issues
- HTML-escape logoValue.path in img src attribute (XSS prevention)
- Add data-file-data attribute to preserve file metadata for serialization
- Add data-prop-key attribute for proper property tracking
- Use schema-driven remove button label (x-removeLabel) with fallback to 'Remove item'
3. base.html addCustomFeedRow: Fix duplicate enabled field and hardcoded pluginId
- Remove duplicate hidden input for enabled field (checkbox alone is sufficient)
- Add pluginId parameter to function signature
- Pass pluginId to handleCustomFeedLogoUpload instead of hardcoded 'ledmatrix-news'
- Update caller in plugin_config.html to pass plugin_id
These fixes improve security (XSS prevention), functionality (metadata
preservation), and maintainability (no hardcoded values).
* fix: Make install_wifi_monitor.sh more resilient to failures
Make install_wifi_monitor.sh handle errors more gracefully:
1. Remove unnecessary sudo when running as root:
- Check EUID before using sudo for systemctl commands
- Use systemctl directly when running as root
- Use sudo only when running as regular user
2. Add error handling for package installation:
- Continue even if apt update fails (just warn)
- Continue even if apt install fails (warn and provide manual install command)
- Allow installation to continue even if packages fail
3. Make service operations more resilient:
- Remove sudo when running as root
- Allow service start to fail without exiting script
- Print warning if service fails to start
- Service will still be enabled and may start on reboot
Note: Script still uses 'set -e' but errors in critical paths are handled
with || operators to prevent exit. This prevents the script from exiting
with code 1 when called from first_time_install.sh, allowing the
installation to continue even if some WiFi-related operations fail.
* fix: Make WiFi monitor installation failure non-fatal in first_time_install.sh
Make the WiFi monitor service installation optional/non-fatal:
1. Capture exit code from install_wifi_monitor.sh but don't fail installation
2. Continue installation even if WiFi monitor installation fails
3. Provide clear messages about the failure but allow installation to proceed
4. Check for service file creation and provide helpful messages
WiFi monitor is optional functionality - the main LED Matrix installation
should succeed even if WiFi monitor setup fails (e.g., package installation
issues, service start failures, etc.). Users can install it later if needed.
This prevents the entire installation from failing at step 8.5 due to
WiFi monitor installation issues.
* fix: Use JSON encoding for bracket-notation arrays and add sentinel for clearing
Fix bracket-notation array handling to prevent data loss:
1. Use JSON encoding instead of comma-join (lines 3358-3359):
- Comma-join breaks if option values contain commas
- Switch to json.dumps() to encode array values as JSON strings
- _parse_form_value_with_schema() already handles JSON arrays correctly
- Preserves values with commas, special characters, etc.
2. Add sentinel hidden input for clearing arrays:
- Add hidden input with name="field[]" value="" in checkbox-group template
- Ensures field is always submitted, even when all checkboxes unchecked
- Backend filters out sentinel empty strings to detect empty array
- Allows users to clear array to [] by unchecking all options
3. Update backend to handle sentinel:
- Filter out sentinel empty strings from bracket notation values
- Empty array (all unchecked) is represented as "[]" JSON string
- Properly handles both sentinel-only (empty array) and sentinel+values cases
This fixes data loss when:
- Option values contain commas (comma-join corruption)
- All checkboxes are unchecked (field omitted from form, can't clear to [])
* fix: Harden upload flow - HTTP status check, path normalization, property assignment
Fix three security and reliability issues in upload flow:
1. Check HTTP status before calling response.json():
- Prevents JSON parsing errors on non-2xx responses
- Properly handles error responses with status codes
- Returns error text if available for better debugging
- Prevents masking of HTTP errors
2. Normalize uploadedFile.path before using in img src:
- Remove leading slashes with replace(/^\/+/, '')
- Add single leading slash for image src
- Prevents //host/odd paths that could cause security issues
- Ensures consistent path format
3. Replace string-based handlers with property assignment:
- Replace setAttribute('onchange', ...) with addEventListener('change', ...)
- Replace setAttribute('onclick', ...) with addEventListener('click', ...)
- Refactor addCustomFeedRow to use DOM manipulation instead of innerHTML
- Prevents injection vulnerabilities from string interpolation
- Uses property assignment (img.src, input.name, input.value) instead of setAttribute where appropriate
These changes improve security by eliminating XSS injection surfaces
and improve reliability by properly handling HTTP errors and path formats.
* fix: Add bracket notation to checkbox-group input names
The backend expects checkbox groups to submit with bracket notation
(request.form.getlist("<field>[]")), but the templates were rendering
checkboxes without the "[]" suffix in the name attribute.
Changes:
1. Add name="{{ full_key }}[]" to checkbox inputs in plugin_config.html
2. Add name="${fullKey}[]" to checkbox inputs in plugins_manager.js
This ensures:
- Checked checkboxes submit their values with the bracket notation
- Backend can use request.form.getlist("<field>[]") to collect all values
- Sentinel hidden input (already using bracket notation) works correctly
- Backend bracket_array_fields logic receives and processes the array values
The sentinel hidden input ensures the field is always submitted (even
when all checkboxes are unchecked), allowing the backend to detect and
set empty arrays correctly.
* fix: Swap order of enabled checkbox and hidden input in custom-feeds
The hidden input with value="false" was rendered before the checkbox,
causing request.form.to_dict() to use the hidden input's value instead
of the checkbox's "true" value when checked.
Fix by rendering the checkbox first, then the hidden fallback input.
This ensures that when the checkbox is checked, its "true" value
overwrites the hidden input's "false" value in request.form.to_dict().
The hidden input still serves as a fallback to ensure "false" is
submitted when the checkbox is unchecked (since unchecked checkboxes
don't submit a value).
* fix: Enable upload buttons for existing custom feed rows in template
The template was rendering disabled upload buttons for existing custom
feed rows with the message "Logo upload for custom feeds is not yet
implemented", while the JavaScript addCustomFeedRow function creates
working upload buttons for newly added rows. This created confusing UX
where users saw disabled buttons on existing feeds but working buttons
on newly added feeds.
Since handleCustomFeedLogoUpload is fully implemented and functional,
enable the upload buttons in the template to match the JavaScript
behavior:
1. Remove disabled and aria-disabled attributes from file input
2. Remove disabled, aria-disabled, misleading title, and update button
styling to match working buttons (remove cursor-not-allowed and
opacity-50, add hover:bg-gray-300)
3. Add onchange handler to file input calling handleCustomFeedLogoUpload
4. Add onclick handler to button to trigger file input click
This ensures consistent UX across existing and newly added custom feed
rows, with all upload buttons functional.
* fix: Expose escapeHtml to window object for use by global functions
The escapeHtml function is defined inside the IIFE (at line 5445) but is
called at line 6508 from within window.addArrayObjectItem, which is
defined outside the IIFE (starting at line 6465). Since escapeHtml is
not exposed to the window object (unlike renderArrayObjectItem and
getSchemaProperty which are exposed at lines 6457-6458), the fallback
code path throws a ReferenceError: escapeHtml is not defined when
window.renderArrayObjectItem is unavailable.
Fix by exposing escapeHtml to the window object alongside
renderArrayObjectItem and getSchemaProperty, ensuring the fallback code
in window.addArrayObjectItem can safely call escapeHtml when the primary
rendering function fails to load.
This prevents users from being unable to add new items to array-of-objects
fields when the primary rendering function is unavailable.
* fix: Escape single quotes in checkbox-group JSON value attribute
The hidden input for checkbox-group uses a single-quoted value attribute
with {{ array_value|tojson|safe }}, but the tojson filter doesn't escape
single quotes for HTML attributes. While JSON uses double quotes for
strings, if array_value contains strings with single quotes (like
"Tom's Choice"), the resulting HTML value='["Tom's Choice"]' could
have parsing issues in some browsers when the single quote appears inside
the JSON string content.
The JavaScript equivalent at line 3037 correctly escapes single quotes
with .replace(/'/g, "'"), but the Jinja2 template lacked this
escaping.
Fix by applying the replace filter to escape single quotes:
{{ (array_value|tojson|safe)|replace("'", "'") }}
This ensures consistent behavior between server-side template rendering
and client-side JavaScript rendering, and prevents potential HTML attribute
parsing issues.
* fix: Move hidden input before checkbox for enabled field in custom-feeds
The hidden input and checkbox share the same name, causing duplicate form
values. When request.form.to_dict() processes multiple fields with the same
name, it uses the LAST value.
The previous fix (a315693b) had the checkbox first and hidden input second,
which meant the hidden input's "false" value would override the checkbox's
"true" value when checked.
Fix by moving the hidden input BEFORE the checkbox, so:
- When checkbox is checked: checkbox value ("true") overrides hidden ("false")
- When checkbox is unchecked: hidden input value ("false") is used (checkbox
doesn't submit a value)
This ensures the correct boolean value is submitted in both cases.
* fix: Use dataset-driven indices for custom feed row reindexing
After removeCustomFeedRow() reindexes data-index/id/name, the existing
file-input change handlers still used stale closure indices, causing
querySelector to fail and preventing logo uploads from working.
Fix by using dataset-driven indices instead of closure-captured values:
1. In addCustomFeedRow:
- Store index in fileInput.dataset.index
- Read index from e.target.dataset.index in event handler
- Use fileInput.click() directly instead of getElementById
2. In removeCustomFeedRow:
- Update dataset.index for all inputs during reindexing
- Remove onclick/onchange attribute rewriting (handlers use addEventListener)
- Simplify ID updating to handle both _logo_<n> and _logo_preview_<n>
3. In handleCustomFeedLogoUpload:
- Store index in fileInput.dataset.index
- Read index from e.target.dataset.index in event handler
- Use fileInput.click() directly
- Set pathInput.value to imageSrc (normalized path)
- Reset event.target.value to allow re-uploading the same file
This ensures event handlers always use the current index from the DOM,
preventing stale closure issues after row removal and reindexing.
* fix: Reset file input value to allow re-uploading same file
Add event.target.value = '' after successful upload to allow re-uploading
the same file (change event won't fire otherwise if the same file is
selected again).
* fix: Add proper attribute escaping for renderArrayObjectItem
The renderArrayObjectItem function was vulnerable because escapeHtml does
not properly escape attribute contexts (quotes). This could lead to XSS
if user-provided data contains quotes or other special characters in
attribute values.
Changes:
1. Create escapeAttribute function for proper attribute escaping
- Escapes quotes, ampersands, and other special characters
- Handles null/undefined values safely
2. Update renderArrayObjectItem to use escapeAttribute for all attribute values:
- id attributes (itemId, propKey)
- data-* attributes (data-prop-key, data-file-data)
- value attributes (input values)
- placeholder attributes
- title attributes
- src attributes (img src)
- onclick/onchange handler parameters (fieldId)
3. Safely encode JSON in data-file-data attribute:
- Use base64 encoding (btoa) instead of manual quote escaping
- Decode with atob when reading the attribute
- This safely handles all characters including quotes, newlines, etc.
4. Remove hardcoded 'ledmatrix-news' pluginId fallback:
- Change fallback from 'ledmatrix-news' to null
- Prevents surprising defaults when uploads are enabled later
- Requires explicit pluginId configuration
This ensures all attribute values are properly escaped and prevents
XSS vulnerabilities from unescaped quotes or special characters.
* fix: Expose escapeAttribute to window object
The escapeAttribute function was not exposed to the window object, which
could cause issues if other code needs to use it. Expose it alongside
escapeHtml for consistency.
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(plugins): Remove compatible_versions requirement from single plugin install
Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.
* fix(7-segment-clock): Update submodule with separator and spacing fixes
* fix(plugins): Add onchange handlers to existing custom feed inputs
- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)
* Add array-of-objects widget support to web UI
- Add support for rendering arrays of objects in web UI (for custom_feeds)
- Implement add/remove/update functions for array-of-objects widgets
- Support file-upload widgets within array items
- Update form data handling to support array JSON data fields
* Update plugins_manager.js cache-busting version
Update version parameter to force browser to load new JavaScript with array-of-objects widget support.
* Fix: Move array-of-objects detection before file-upload/checkbox checks
Move the array-of-objects widget detection to the top of the array handler so it's checked before file-upload and checkbox-group widgets. This ensures custom_feeds is properly detected as an array of objects.
* Update cache-busting version for array-of-objects fix
* Remove duplicate array-of-objects check
* Update cache version again
* Add array-of-objects widget support to server-side template
Add detection and rendering for array-of-objects in the Jinja2 template (plugin_config.html).
This enables the custom_feeds widget to display properly with name, URL, enabled checkbox, and logo upload fields.
The widget is detected by checking if prop.items.type == 'object' && prop.items.properties,
and is rendered before the file-upload widget check.
* Use window. prefix for array-of-objects JavaScript functions
Explicitly use window.addArrayObjectItem, window.removeArrayObjectItem, etc.
in the template to ensure the functions are accessible from inline event handlers.
Also add safety checks to prevent errors if functions aren't loaded yet.
* Fix syntax error: Missing indentation for html += in array else block
The html += statement was outside the else block, causing a syntax error.
Fixed by properly indenting it inside the else block.
* Update cache version for syntax fix
* Add debug logging to diagnose addArrayObjectItem availability
* Fix: Wrap array-of-objects functions in window check and move outside IIFE
Ensure functions are available globally by wrapping them in a window check
and ensuring they're defined outside any IIFE scope. Also fix internal
function calls to use window.updateArrayObjectData for consistency.
* Update cache version for array-of-objects fix
* Move array-of-objects functions outside IIFE to make them globally available
The functions were inside the IIFE scope, making them inaccessible from
inline event handlers. Moving them outside the IIFE ensures they're
available on window when the script loads.
* Update cache version for IIFE fix
* Fix: Add array-of-objects functions after IIFE ends
The functions were removed from inside the IIFE but never added after it.
Also removed orphaned code that was causing syntax errors.
* Update cache version for array-of-objects fix
* Fix: Remove all orphaned code and properly add array-of-objects functions after IIFE
* Add array-of-objects functions after IIFE ends
These functions must be outside the IIFE to be accessible from inline
event handlers in the server-rendered template.
* Update cache version for syntax fix
* Fix syntax error: Add missing closing brace for else block
* Update cache version for syntax fix
* Replace complex array-of-objects widget with simple table interface
- Replace nested array-of-objects widget with clean table interface
- Table shows: Name, URL, Logo (with upload), Enabled checkbox, Delete button
- Fix file-upload widget detection order to prevent breaking static-image plugin
- Add simple JavaScript functions for add/remove rows and logo upload
- Much more intuitive and easier to use
* Add simple table interface for custom feeds
- Replace complex array-of-objects widget with clean table
- Table columns: Name, URL, Logo (upload), Enabled checkbox, Delete
- Use dot notation for form field names (feeds.custom_feeds.0.name)
- Add JavaScript functions for add/remove rows and logo upload
- Fix file-upload detection order to prevent breaking static-image plugin
* Fix custom feeds table issues
- Fix JavaScript error in removeCustomFeedRow (get tbody before removing row)
- Improve array conversion logic to handle nested paths like feeds.custom_feeds
- Add better error handling and debug logging for array conversion
- Ensure dicts with numeric keys are properly converted to arrays before validation
* Add fallback fix for feeds.custom_feeds dict-to-array conversion
- Add explicit fallback conversion for feeds.custom_feeds if fix_array_structures misses it
- This ensures the dict with numeric keys is converted to an array before validation
- Logo field is already optional in schema (not in required array)
* feat(web): Add checkbox-group widget support for plugin config arrays
Add server-side rendering support for checkbox-group widget in plugin
configuration forms. This allows plugins to use checkboxes for multi-select
array fields instead of comma-separated text inputs.
The implementation:
- Checks for x-widget: 'checkbox-group' in schema
- Renders checkboxes for each enum item in items.enum
- Supports custom labels via x-options.labels
- Works with any plugin that follows the pattern
Already used by:
- ledmatrix-news plugin (enabled_feeds)
- odds-ticker plugin (enabled_leagues)
* feat(install): Add one-shot installation script
- Create comprehensive one-shot installer with robust error handling
- Includes network checks, disk space validation, and retry logic
- Handles existing installations gracefully (idempotent)
- Updates README with quick install command prominently featured
- Manual installation instructions moved to collapsible section
The script provides explicit error messages and never fails silently.
All prerequisites are validated before starting installation.
* fix: Remove accidental plugins/7-segment-clock submodule entry
Remove uninitialized submodule 'plugins/7-segment-clock' that was
accidentally included. This submodule is not related to the one-shot
installer feature and should not be part of this PR.
- Remove submodule entry from .gitmodules
- Remove submodule from git index
- Clean up submodule configuration
* fix(array-objects): Fix schema lookup, reindexing, and disable file upload
Address PR review feedback for array-of-objects helpers:
1. Schema resolution: Use getSchemaProperty() instead of manual traversal
- Fixes nested array-of-objects schema lookup (e.g., news.custom_feeds)
- Now properly descends through .properties for nested objects
2. Reindexing: Replace brittle regex with targeted patterns
- Only replace index in bracket notation [0], [1], etc. for names
- Only replace _item_<digits> pattern for IDs (not arbitrary digits)
- Use specific function parameter patterns for onclick handlers
- Prevents corruption of fieldId, pluginId, or other numeric values
3. File upload: Disable widget until properly implemented
- Hide/disable upload button with clear message
- Show existing logos if present but disable upload functionality
- Prevents silent failures when users attempt to upload files
- Added TODO comments for future implementation
Also fixes exit code handling in one-shot-install.sh to properly capture
first_time_install.sh exit status before error trap fires.
* fix(security): Fix XSS vulnerability in handleCustomFeedLogoUpload
Replace innerHTML usage with safe DOM manipulation using createElement
and setAttribute to prevent XSS when injecting uploadedFile.path and
uploadedFile.id values.
- Clear logoCell using textContent instead of innerHTML
- Create all DOM elements using document.createElement
- Set uploadedFile.path and uploadedFile.id via setAttribute (automatically escaped)
- Properly structure DOM tree by appending elements in order
- Prevents malicious HTML/script injection through file path or ID values
* fix: Update upload button onclick when reindexing custom feed rows
Fix removeCustomFeedRow to update button onclick handlers that reference
file input IDs with _logo_<index> when rows are reindexed after deletion.
Previously, after deleting a row, the upload button's onclick still referenced
the old file input ID, causing the upload functionality to fail.
Now properly updates:
- getElementById('..._logo_<num>') patterns in onclick handlers
- Other _logo_<num> patterns in button onclick strings
- Function parameter indices in onclick handlers
This ensures upload buttons continue to work correctly after row deletion.
* fix: Make custom feeds table widget-specific instead of generic fallback
Replace generic array-of-objects check with widget-specific check for
'custom-feeds' widget to prevent hardcoded schema from breaking other
plugins with different array-of-objects structures.
Changes:
- Check for x-widget == 'custom-feeds' before rendering custom feeds table
- Add schema validation to ensure required fields (name, url) exist
- Show warning message if schema doesn't match expected structure
- Fall back to generic array input for other array-of-objects schemas
- Add comments for future generic array-of-objects support
This ensures the hardcoded custom feeds table (name, url, logo, enabled)
only renders when explicitly requested via widget type, preventing
breakage for other plugins with different array-of-objects schemas.
* fix: Add image/gif to custom feed logo upload accept attribute
Update file input accept attributes for custom feed logo uploads to include
image/gif, making it consistent with the file-upload widget which also
allows GIF images.
Updated in three places:
- Template file input (plugin_config.html)
- JavaScript addCustomFeedRow function (base.html)
- Dynamic file input creation in handleCustomFeedLogoUpload (base.html)
All custom feed logo upload inputs now accept: image/png, image/jpeg,
image/bmp, image/gif
* fix: Add hidden input for enabled checkbox to ensure false is submitted
Add hidden input with value='false' before enabled checkbox in custom feeds
table to ensure an explicit false value is sent when checkbox is unchecked.
Pattern implemented:
- Hidden input: name='enabled', value='false' (always submitted)
- Checkbox: name='enabled', value='true' (only submitted when checked)
- When unchecked: only hidden input submits (false)
- When checked: both submit, checkbox value (true) overwrites hidden
Updated in two places:
- Template checkbox in plugin_config.html (existing rows)
- JavaScript addCustomFeedRow function in base.html (new rows)
Backend verification:
- Backend (api_v3.py) handles string boolean values and converts properly
- JavaScript form processing explicitly checks element.checked, independent of this pattern
- Standard form submission uses last value when multiple values share same name
* fix: Expose renderArrayObjectItem to window for addArrayObjectItem
Fix scope issue where renderArrayObjectItem is defined inside IIFE but
window.addArrayObjectItem is defined outside, causing the function check
to always fail and fallback to degraded HTML rendering.
Problem:
- renderArrayObjectItem (line 2469) is inside IIFE (lines 796-6417)
- window.addArrayObjectItem (line 6422) is outside IIFE
- Check 'typeof renderArrayObjectItem === function' at line 6454 always fails
- Fallback code lacks file upload widgets, URL input types, descriptions, styling
Solution:
- Expose renderArrayObjectItem to window object before IIFE closes
- Function maintains closure access to escapeHtml and other IIFE-scoped functions
- Newly added items now have full functionality matching initially rendered items
* fix: Reorder array type checks to match template order
Fix inconsistent rendering where JavaScript and Jinja template had opposite
ordering for array type checks, causing schemas with both x-widget: file-upload
AND items.type: object (like static-image) to render differently.
Problem:
- Template checks file-upload FIRST (to avoid breaking static-image plugin)
- JavaScript checked array-of-objects FIRST
- Server-rendered forms showed file-upload widget correctly
- JS-rendered forms incorrectly displayed array-of-objects table widget
Solution:
- Reorder JavaScript checks to match template order:
1. Check file-upload widget FIRST
2. Check checkbox-group widget
3. Check custom-feeds widget
4. Check array-of-objects as fallback
5. Regular array input (comma-separated)
This ensures consistent rendering between server-rendered and JS-rendered forms
for schemas that have both x-widget: file-upload AND items.type: object.
* fix: Handle None value for feeds config to prevent TypeError
Fix crash when plugin_config['feeds'] exists but is None, causing
TypeError when checking 'custom_feeds' in feeds_config.
Problem:
- When plugin_config['feeds'] exists but is None, dict.get('feeds', {})
returns None (not the default {}) because dict.get() only uses default
when key doesn't exist, not when value is None
- Line 3642's 'custom_feeds' in feeds_config raises TypeError because
None is not iterable
- This can crash the API endpoint if a plugin config has feeds: null
Solution:
- Change plugin_config.get('feeds', {}) to plugin_config.get('feeds') or {}
to ensure feeds_config is always a dict (never None)
- Add feeds_config check before 'in' operator for extra safety
This ensures the code gracefully handles feeds: null in plugin configuration.
* fix: Add default value for AVAILABLE_SPACE to prevent TypeError
Fix crash when df produces unexpected output that results in empty
AVAILABLE_SPACE variable, causing 'integer expression expected' error.
Problem:
- df may produce unexpected output format (different locale, unusual
filesystem name spanning lines, or non-standard df implementation)
- While '|| echo "0"' handles pipeline failures, it doesn't trigger if
awk succeeds but produces no output (empty string)
- When AVAILABLE_SPACE is empty, comparison [ "$AVAILABLE_SPACE" -lt 500 ]
fails with 'integer expression expected' error
- With set -e, this causes script to exit unexpectedly
Solution:
- Add AVAILABLE_SPACE=${AVAILABLE_SPACE:-0} before comparison to ensure
variable always has a numeric value (defaults to 0 if empty)
- This gracefully handles edge cases where df/awk produces unexpected output
* fix: Wrap debug console.log in debug flag check
Fix unconditional debug logging that outputs internal implementation
details to browser console for all users.
Problem:
- console.log('[ARRAY-OBJECTS] Functions defined on window:', ...)
executes unconditionally when page loads
- Outputs debug information about function availability to all users
- Appears to be development/debugging code inadvertently included
- Noisy console output in production
Solution:
- Wrap console.log statement in _PLUGIN_DEBUG_EARLY check to only
output when pluginDebug localStorage flag is enabled
- Matches pattern used elsewhere in the file for debug logging
- Debug info now only visible when explicitly enabled via
localStorage.setItem('pluginDebug', 'true')
* fix: Expose getSchemaProperty, disable upload widget, handle bracket notation arrays
Multiple fixes for array-of-objects and form processing:
1. Expose getSchemaProperty to window (plugins_manager.js):
- getSchemaProperty was defined inside IIFE but needed by global functions
- Added window.getSchemaProperty = getSchemaProperty before IIFE closes
- Updated window.addArrayObjectItem to use window.getSchemaProperty
- Fixes ReferenceError when dynamically adding array items
2. Disable upload widget for custom feeds (plugin_config.html):
- File input and Upload button were still active but should be disabled
- Removed onchange/onclick handlers, added disabled and aria-disabled
- Added visible disabled styling and tooltip
- Existing logos continue to display but uploads are prevented
- Matches PR objectives to disable upload until fully implemented
3. Handle bracket notation array fields (api_v3.py):
- checkbox-group uses name="field_name[]" which sends multiple values
- request.form.to_dict() collapses duplicate keys (only keeps last value)
- Added handling to detect fields ending with "[]" before to_dict()
- Use request.form.getlist() to get all values, combine as comma-separated
- Processed before existing array index field handling
- Fixes checkbox-group losing all but last selected value
* fix: Remove duplicate submit handler to prevent double POSTs
Remove document-level submit listener that conflicts with handlePluginConfigSubmit,
causing duplicate form submissions with divergent payloads.
Problem:
- handlePluginConfigSubmit correctly parses JSON from _data fields and maps to
flatConfig[baseKey] for patternProperties and array-of-objects
- Document-level listener (line 5368) builds its own config without understanding
_data convention and posts independently via savePluginConfiguration
- Every submit now sends two POSTs with divergent payloads:
- First POST: Correct structure with parsed _data fields
- Second POST: Incorrect structure with raw _data fields, missing structure
- Arrays-of-objects and patternProperties saved incorrectly in second request
Solution:
- Remove document-level submit listener for #plugin-config-form
- Rely solely on handlePluginConfigSubmit which is already attached to the form
- handlePluginConfigSubmit properly handles all form-to-config conversion including:
- _data field parsing (JSON from hidden fields)
- Type-aware conversion using schema
- Dot notation to nested object conversion
- PatternProperties and array-of-objects support
Note: savePluginConfiguration function remains for use by JSON editor saves
* fix: Use indexed names for checkbox-group to work with existing parser
Change checkbox-group widget to use indexed field names instead of bracket
notation, so the existing indexed field parser correctly handles multiple
selected values.
Problem:
- checkbox-group uses name="{{ full_key }}[]" which requires bracket
notation handling in backend
- While bracket notation handler exists, using indexed names is more robust
and leverages existing well-tested indexed field parser
- Indexed field parser already handles fields like "field_name.0",
"field_name.1" correctly
Solution:
- Template: Change name="{{ full_key }}[]" to name="{{ full_key }}.{{
loop.index0 }}"
- JavaScript: Update checkbox-group rendering to use name="."
- Backend indexed field parser (lines 3364-3388) already handles this pattern:
- Detects fields ending with numeric indices (e.g., ".0", ".1")
- Groups them by base_path and sorts by index
- Combines into array correctly
This ensures checkbox-group values are properly preserved when multiple
options are selected, working with the existing schema-based parsing system.
* fix: Set values from item data in fallback array-of-objects rendering
Fix fallback code path for rendering array-of-objects items to properly
set input values from existing item data, matching behavior of proper
renderArrayObjectItem function.
Problem:
- Fallback code at lines 3078-3091 and 6471-6486 creates input elements
without setting values from existing item data
- Text inputs have no value attribute set
- Checkboxes have no checked attribute computed from item properties
- Users would see empty form fields instead of existing configuration data
- Proper renderArrayObjectItem function correctly sets values (line 2556)
Solution:
- Extract propValue from item data: item[propKey] with schema default fallback
- For text inputs: Set value attribute with HTML-escaped propValue
- For checkboxes: Set checked attribute based on propValue truthiness
- Add inline HTML escaping for XSS prevention (since fallback code may
run outside IIFE scope where escapeHtml function may not be available)
This ensures fallback rendering displays existing data correctly when
window.renderArrayObjectItem is not available.
* fix: Remove extra closing brace breaking if/else chain
Remove stray closing brace at line 3127 that was breaking the if/else chain
before the 'else if (prop.enum)' branch, causing 'Unexpected token else'
syntax error.
Problem:
- Extra '}' at line 3127 closed the prop.type === 'array' block prematurely
- This broke the if/else chain, causing syntax error when parser reached
'else if (prop.enum)' at line 3128
- Structure was: } else if (array) { ... } } } else if (enum) - extra brace
Solution:
- Removed the extra closing brace at line 3127
- Structure now correctly: } else if (array) { ... } } else if (enum)
- Verified with Node.js syntax checker - no errors
* fix: Remove local logger assignments to prevent UnboundLocalError
Remove all local logger assignments inside save_plugin_config function that
were shadowing the module-level logger, causing UnboundLocalError when nested
helpers like normalize_config_values() or debug checks reference logger before
those assignments run.
Problem:
- Module-level logger exists at line 13: logger = logging.getLogger(__name__)
- Multiple local assignments inside save_plugin_config (lines 3361, 3401, 3421,
3540, 3660, 3977, 4093, 4118) make logger a local variable for entire function
- Python treats logger as local for entire function scope when any assignment
exists, causing UnboundLocalError if logger is used before assignments
- Nested helpers like normalize_config_values() or debug checks that reference
logger before local assignments would fail
Solution:
- Removed all local logger = logging.getLogger(__name__) assignments in
save_plugin_config function
- Use module-level logger directly throughout the function
- Removed redundant import logging statements that were only used for logger
- This ensures logger is always available and references the module-level logger
All logger references now use the module-level logger without shadowing.
* fix: Fix checkbox-group serialization and array-of-objects key leakage
Multiple fixes for array-of-objects and checkbox-group widgets:
1. Fix checkbox-group serialization (JS and template):
- Changed from indexed names (categories.0, categories.1) to _data pattern
- Added updateCheckboxGroupData() function to sync selected values
- Hidden input stores JSON array of selected enum values
- Checkboxes use data-checkbox-group and data-option-value attributes
- Fixes issue where config.categories became {0: true, 1: true} instead of ['nfl', 'nba']
- Now correctly serializes to array using existing _data handling logic
2. Prevent array-of-objects per-item key leakage:
- Added skip pattern in handlePluginConfigSubmit for _item_<n>_ names
- Removed name attributes from per-item inputs in renderArrayObjectItem
- Per-item inputs now rely solely on hidden _data field
- Prevents feeds_item_0_name from leaking into flatConfig
3. Add type coercion to updateArrayObjectData:
- Consults itemsSchema.properties[propKey].type for coercion
- Handles integer and number types correctly
- Preserves string values as-is
- Ensures numeric fields in array items are stored as numbers
4. Ensure currentPluginConfig is always available:
- Updated addArrayObjectItem to check window.currentPluginConfig first
- Added error logging if schema not available
- Prevents ReferenceError when global helpers need schema
This ensures checkbox-group arrays serialize correctly and array-of-objects
per-item fields don't leak extra keys into the configuration.
* fix: Make _data field matching more specific to prevent false positives
Fix overly broad condition that matched any field containing '_data',
causing false positives and inconsistent key transformation.
Problem:
- Condition 'key.endsWith('_data') || key.includes('_data')' matches any
field containing '_data' anywhere (e.g., 'meta_data_field', 'custom_data_config')
- key.replace(/_data$/, '') only removes '_data' from end, making logic inconsistent
- Fields with '_data' in middle get matched but key isn't transformed
- If their value happens to be valid JSON, it gets incorrectly parsed
Solution:
- Remove 'key.includes('_data')' clause
- Only check 'key.endsWith('_data')' to match actual _data suffix pattern
- Ensures consistent matching: only fields ending with '_data' are treated
as JSON data fields, and only those get the suffix removed
- Prevents false positives on fields like 'meta_data_field' that happen to
contain '_data' in their name
* fix: Add HTML escaping to prevent XSS in fallback code and checkbox-group
Add proper HTML escaping for schema-derived values to prevent XSS vulnerabilities
in fallback rendering code and checkbox-group widget.
Problem:
- Fallback code in generateFieldHtml (line 3094) doesn't escape propLabel
when building HTML strings, while main renderArrayObjectItem uses escapeHtml()
- Checkbox-group widget (lines 3012-3025) doesn't escape option or label values
- While risk is limited (values come from plugin schemas), malicious plugin
schemas or untrusted schema sources could inject XSS
- Inconsistent with main renderArrayObjectItem which properly escapes
Solution:
- Added escapeHtml() calls for propLabel in fallback array-of-objects rendering
(both locations: generateFieldHtml and addArrayObjectItem fallback)
- Added escapeHtml() calls for option values in checkbox-group widget:
- checkboxId (contains option)
- data-option-value attribute
- value attribute
- label text in span
- Ensures consistent XSS protection across all rendering paths
This prevents potential XSS if plugin schemas contain malicious HTML/script
content in enum values or property titles.
---------
Co-authored-by: Chuck <chuck@example.com>
Removed detailed configuration sections for Calendar Display, Odds Ticker, Stocks, Football, Music Display, and Cache Information from README.
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
* fix(plugins): Remove compatible_versions requirement from single plugin install
Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.
* fix(7-segment-clock): Update submodule with separator and spacing fixes
* fix(plugins): Add onchange handlers to existing custom feed inputs
- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)
---------
Co-authored-by: Chuck <chuck@example.com>
Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.
Co-authored-by: Chuck <chuck@example.com>
* fix(plugins): Fix GitHub install button for single plugin installation
- Clone install button before attaching event listener to prevent duplicate handlers
- Add safety checks for pluginStatusDiv element
- Move installFromCustomRegistry function definition earlier in file
- Add error logging when button/elements not found
- Ensure consistent button reference usage in event handlers
Fixes issue where Install button in 'Install Single Plugin' section
was not working properly.
* fix(plugins): Add button type and better logging for install button
- Add type='button' to install button to prevent form submission
- Add console logging to debug click handler attachment
- Add preventDefault and stopPropagation to click handler
- Improve error logging for debugging
* fix(plugins): Re-attach install button handler when section is shown
- Extract install button handler to separate function
- Re-attach handler when GitHub install section is toggled visible
- Add data attribute to prevent duplicate handler attachments
- Add comprehensive logging for debugging
- Handler now attaches even if section starts hidden
* fix(plugins): Add comprehensive logging to debug install button handler
- Add logging at function entry points
- Add logging when section is shown and handler re-attached
- Add logging before and after calling attachInstallButtonHandler
- Helps diagnose why handler isn't being attached
* fix(plugins): Expose GitHub install handlers globally and add fallback
- Expose setupGitHubInstallHandlers and attachInstallButtonHandler to window object
- Add fallback handler attachment after page load delay
- Fix typo in getElementById call
- Allows manual testing from browser console
- Ensures handlers are accessible even if IIFE scope issues occur
* fix(plugins): Add fallback handler attachment after page load
* fix(plugins): Ensure GitHub install handlers are set up even if already initialized
- Add check to verify setupGitHubInstallHandlers exists before calling
- Call setupGitHubInstallHandlers even if initializePlugins was already called
- Add comprehensive logging to track function execution
- Helps diagnose why handlers aren't being attached
* fix(plugins): Add more prominent logging markers for easier debugging
* fix(plugins): Add simple standalone handler for GitHub plugin installation
- Create handleGitHubPluginInstall() function defined early and globally
- Add inline onclick handler to button as fallback
- Bypasses complex initialization flow and IIFE scope issues
- Direct approach that works immediately without dependencies
- Provides clear error messages and logging
* chore: Update 7-segment-clock plugin submodule
- Update to latest version with scaling support
- Includes compatible_versions field fix for plugin store installation
* fix(plugins): Add update and uninstall handling to global event delegation fallback
- Add 'update' action handling in handleGlobalPluginAction fallback
- Add 'uninstall' action handling with confirmation dialog
- Fixes issue where update/uninstall buttons did nothing
- Buttons now work even if handlePluginAction isn't available yet
* fix(plugins): Improve error message for plugin updates from GitHub URLs
- Check if plugin is a git repository before checking registry
- Provide more accurate error messages for plugins installed from URLs
- Fixes misleading 'Plugin not found in registry' error for git-based plugins
- Update should work for plugins installed from GitHub URLs even if not in registry
* fix(plugins): Add detailed logging for plugin update failures
- Log git command that failed and return code
- Add logging before/after update attempt
- Log whether plugin is detected as git repository
- Helps diagnose why updates fail for plugins installed from URLs
* fix(plugins): Add better logging for plugin update detection
- Log when plugin is detected as git repository
- Log when plugin is not a git repository
- Provide helpful message for ZIP-installed plugins
- Helps diagnose why updates fail for plugins installed from URLs
* fix(plugins): Enable updates for plugins installed from GitHub URLs
- Get git remote URL from plugin directory even if .git is missing
- If plugin not in registry but has remote URL, reinstall as git repo
- Allows updating plugins installed from URLs even if git clone failed initially
- Falls back to reinstalling from original URL to enable future updates
* fix(plugins): Reinstall from git remote URL if plugin not in registry
- When plugin is not a git repo and not in registry, check for git remote URL
- If remote URL exists, reinstall plugin from that URL to enable future updates
- Handles case where plugin was installed from URL but git clone failed initially
* fix(plugins): Improve git update error handling and logging
- Make git fetch non-fatal (log warning but continue)
- Make git checkout non-fatal (log warning but continue)
- Add detailed error messages for common git failures
- Log which git command failed and return code
- Better handling of authentication, merge conflicts, and unrelated histories
* fix(plugins): Add detailed exception logging to update endpoint
- Log full traceback when update fails
- Log exception details in catch block
- Helps diagnose update failures from API endpoint
* fix(plugins): Handle untracked files during plugin update
- Remove .dependencies_installed marker file before pull (safe to regenerate)
- Stash untracked files using 'git stash -u' if they can't be removed
- Prevents 'untracked files would be overwritten' errors during update
- Fixes issue where .dependencies_installed blocks git pull
* chore: Update 7-segment-clock submodule with improved clone instructions
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(web): Resolve font display and config API error handling issues
- Fix font catalog display error where path.startsWith fails
(path is object, not string)
- Update save_main_config to use error_response() helper
- Improve save_raw_main_config error handling consistency
- Add proper error codes and traceback details to API responses
* fix(web): Prevent fontCatalog redeclaration error on HTMX reload
- Use window object to store global font variables
- Check if script has already loaded before declaring variables
- Update both window properties and local references on assignment
- Fixes 'Identifier fontCatalog has already been declared' error
* fix(web): Wrap fonts script in IIFE to prevent all redeclaration errors
- Wrap entire script in IIFE that only runs once
- Check if script already loaded before declaring variables/functions
- Expose initializeFontsTab to window for re-initialization
- Prevents 'Identifier has already been declared' errors on HTMX reload
* fix(web): Exempt config save API endpoints from CSRF protection
- Exempt save_raw_main_config, save_raw_secrets_config, and save_main_config from CSRF
- These endpoints are called via fetch from JavaScript and don't include CSRF tokens
- Fixes 500 error when saving config via raw JSON editor
* fix(web): Exempt system action endpoint from CSRF protection
- Exempt execute_system_action from CSRF
- Fixes 500 error when using system action buttons (restart display, restart Pi, etc.)
- These endpoints are called via HTMX and don't include CSRF tokens
* fix(web): Exempt all API v3 endpoints from CSRF protection
- Add before_request handler to exempt all api_v3.* endpoints
- All API endpoints are programmatic (HTMX/fetch) and don't include CSRF tokens
- Prevents future CSRF errors on any API endpoint
- Cleaner than exempting individual endpoints
* refactor(web): Remove CSRF protection for local-only application
- CSRF is designed for internet-facing apps to prevent cross-site attacks
- For local-only Raspberry Pi app, threat model is different
- All endpoints were exempted anyway, so it wasn't protecting anything
- Forms use HTMX without CSRF tokens
- If exposing to internet later, can re-enable with proper token implementation
* fix(web): Fix font path double-prefixing in font catalog display
- Only prefix with 'assets/fonts/' if path is a bare filename
- If path starts with '/' (absolute) or 'assets/' (already prefixed), use as-is
- Fixes double-prefixing when get_fonts_catalog returns relative paths like 'assets/fonts/press_start.ttf'
* fix(web): Remove fontsTabInitialized guard to allow re-initialization on HTMX reload
- Remove fontsTabInitialized check that prevented re-initialization on HTMX content swap
- The window._fontsScriptLoaded guard is sufficient to prevent function redeclaration
- Allow initializeFontsTab() to run on each HTMX swap to attach listeners to new DOM elements
- Fixes fonts UI breaking after HTMX reload (buttons, upload dropzone, etc. not working)
* fix(api): Preserve empty strings for optional string fields in plugin config
- Add _is_field_required() helper to check if fields are required in schema
- Update _parse_form_value_with_schema() to preserve empty strings for optional string fields
- Fixes 400 error when saving MQTT plugin config with empty username/password
- Resolves validation error: 'Expected type string, got NoneType'
* fix(config): Add defaults to schemas and fix None value handling
- Updated merge_with_defaults to replace None values with defaults
- Fixed form processing to skip empty optional fields without defaults
- Added script to automatically add defaults to all plugin config schemas
- Added defaults to 89 fields across 10 plugin schemas
- Prevents validation errors from None values in configs
Changes:
- schema_manager.py: Enhanced merge_with_defaults to replace None with defaults
- api_v3.py: Added _SKIP_FIELD sentinel to skip optional fields without defaults
- add_defaults_to_schemas.py: Script to add sensible defaults to schemas
- Plugin schemas: Added defaults for number, boolean, and array fields
* fix(config): Fix save button spinner by checking HTTP status code
- Fixed handleConfigSave to check xhr.status instead of event.detail.successful
- With hx-swap="none", HTMX doesn't set event.detail.successful
- Now properly detects successful saves (status 200-299) and stops spinner
- Improved error message extraction from API responses
- Also fixed handleToggleResponse for consistency
* fix(web-ui): Resolve GitHub token warning persistence after save
- Made checkGitHubAuthStatus() return Promise for proper async handling
- Clear sessionStorage dismissal flag when token is saved
- Add delay before status check to ensure backend token reload
- Wait for status check completion before hiding settings panel
Fixes issue where GitHub token warnings and pop-ups would not
disappear after successfully saving a token in the web UI.
* fix(web-ui): Add token validation and improve GitHub token warning behavior
- Add token validation to backend API endpoint to check if token is valid/expired
- Implement _validate_github_token() method in PluginStoreManager with caching
- Update frontend to show warning only when token is missing or invalid
- Keep settings panel accessible (collapsible) when token is configured
- Collapse settings panel content after successful token save instead of hiding
- Display specific error messages for invalid/expired tokens
- Clear sessionStorage dismissal flag when token becomes valid
Fixes issue where GitHub token warnings and settings panel would not
properly hide/show based on token status. Now validates token validity
and provides better UX with collapsible settings panel.
* fix(web-ui): Fix CSS/display issue for GitHub token warning and settings
- Update all hide/show operations to use both classList and style.display
- Fix checkGitHubAuthStatus() to properly hide/show warning and settings
- Fix dismissGithubWarning() to use both methods
- Fix toggleGithubTokenSettings() with improved state checking
- Fix collapse button handler with improved state checking
- Fix saveGithubToken() to properly show/collapse settings panel
This ensures elements actually hide/show when status changes, matching
the pattern used elsewhere in the codebase (like toggleSection). All
buttons (dismiss, close, collapse) should now work correctly.
* fix(web-ui): Fix GitHub token expand button functionality
- Convert collapse button handler to named function (toggleGithubTokenContent)
- Improve state checking using class, inline style, and computed style
- Re-attach event listener after saving token to ensure it works
- Add console logging for debugging
- Make function globally accessible for better reliability
Fixes issue where expand button didn't work after saving token.
* fix(web-ui): Remove X button and improve GitHub token panel behavior
- Remove X (close) button from GitHub token configuration panel
- Replace toggleGithubTokenSettings() with openGithubTokenSettings() that only opens
- Auto-collapse panel when token is valid (user must click expand to edit)
- Auto-detect token status on page load (no need to click save)
- Simplify saveGithubToken() to rely on checkGitHubAuthStatus() for UI updates
- Ensure expand button works correctly with proper event listener attachment
The panel now remains visible but collapsed when a token is configured,
allowing users to expand it when needed without the ability to completely hide it.
* refactor(web-ui): Improve GitHub token collapse button code quality
- Update comment to reflect actual behavior (prevent parent click handlers)
- Use empty string for display to defer to CSS instead of hard-coding block/none
- Extract duplicate clone-and-attach logic into attachGithubTokenCollapseHandler() helper
- Make helper function globally accessible for reuse in checkGitHubAuthStatus()
Improves maintainability and makes code more future-proof for layout changes.
* fix(web-ui): Fix collapse/expand button by using removeProperty for display
- Use style.removeProperty('display') instead of style.display = ''
- This properly removes inline styles and defers to CSS classes
- Fixes issue where collapse/expand button stopped working after refactor
* fix(web-ui): Make display handling consistent for token collapse
- Use removeProperty('display') consistently in all places
- Fix checkGitHubAuthStatus() to use removeProperty instead of inline style
- Simplify state checking to rely on hidden class with computed style fallback
- Ensures collapse/expand button works correctly by deferring to CSS classes
* fix(web-ui): Fix token collapse button and simplify state detection
- Simplify state checking to rely on hidden class only (element has class='block')
- Only remove inline display style if it exists (check before removing)
- Add console logging to debug handler attachment
- Ensure collapse/expand works by relying on CSS classes
Fixes issues where:
- Collapse button did nothing
- Auto-detection of token status wasn't working
* debug(web-ui): Add extensive debugging for token collapse button
- Add console logs to track function calls and element detection
- Improve state detection to use computed style as fallback
- Add wrapper function for click handler to ensure it's called
- Better error messages to identify why handler might not attach
This will help identify why the collapse button isn't working.
* debug(web-ui): Add comprehensive debugging for GitHub token features
- Add console logs to checkGitHubAuthStatus() to track execution
- Re-attach collapse handler after plugin store is rendered
- Add error stack traces for better debugging
- Ensure handler is attached when content is dynamically loaded
This will help identify why:
- Auto-detection of token status isn't working
- Collapse button isn't functioning
* fix(web-ui): Move checkGitHubAuthStatus before IIFE to fix scope issue
- Move checkGitHubAuthStatus function definition before IIFE starts
- Function was defined after IIFE but called inside it, causing it to be undefined
- Now function is available when called during initialization
- This should fix auto-detection of token status on page load
* debug(web-ui): Add extensive logging to GitHub token functions
- Add logging when checkGitHubAuthStatus is defined
- Add logging when function is called during initialization
- Add logging in attachGithubTokenCollapseHandler
- Add logging in store render callback
- This will help identify why functions aren't executing
* fix(web-ui): Move GitHub token functions outside IIFE for availability
- Move attachGithubTokenCollapseHandler and toggleGithubTokenContent outside IIFE
- These functions need to be available when store renders, before IIFE completes
- Add logging to initializePlugins to track when it's called
- This should fix the 'undefined' error when store tries to attach handlers
* fix(web-ui): Fix GitHub token content collapse/expand functionality
- Element has 'block' class in HTML which conflicts with 'hidden' class
- When hiding: add 'hidden', remove 'block', set display:none inline
- When showing: remove 'hidden', add 'block', remove inline display
- This ensures proper visibility toggle for the GitHub API Configuration section
* feat(display): Implement on-demand plugin filtering with restart
- Add on-demand plugin filtering to DisplayController initialization
- Filters available_modes to only include on-demand plugin's modes
- Allows plugin internal rotation (e.g., NFL upcoming, NCAA FB Recent)
- Prevents rotation to other plugins
- Implement restart mechanism for on-demand activation/clear
- _restart_with_on_demand_filter() saves state and restarts with filter
- _restart_without_on_demand_filter() restores normal operation
- Supports both systemd service and direct process execution
- Add state preservation across restarts
- Saves/restores rotation position from cache
- Restores on-demand config from cache after restart
- Add service detection method
- Detects if running as systemd service
- Uses file-based approach for environment variable passing
- Update API endpoints with restart flow comments
- Update systemd service file with on-demand support notes
- Add comprehensive error handling for edge cases
* perf(web-ui): Optimize GitHub token detection speed
- Call checkGitHubAuthStatus immediately when script loads (if elements exist)
- Call it early in initPluginsPage (before full initialization completes)
- Use requestAnimationFrame instead of setTimeout(100ms) for store render callback
- Reduce save token delay from 300ms to 100ms
- Token detection now happens in parallel with other initialization tasks
- This makes token status visible much faster on page load
* fix(ui): Move on-demand modal to base.html for always-available access
- Move on-demand modal from plugins.html to base.html
- Ensures modal is always in DOM when Run On-Demand button is clicked
- Fixes issue where button in plugin_config.html couldn't find modal
- Modal is now available regardless of which tab is active
* fix(ui): Initialize on-demand modal unconditionally on page load
- Create initializeOnDemandModal() function that runs regardless of plugins tab
- Modal is in base.html so it should always be available
- Call initialization on DOMContentLoaded and with timeout
- Fixes 'On-demand modal elements not found' error when clicking button
- Modal setup now happens even if plugins tab hasn't been loaded yet
* fix(ui): Add safety check for updatePluginTabStates function
- Check if updatePluginTabStates exists before calling
- Prevents TypeError when function is not available
- Fixes error when clicking plugin tabs
* fix(ui): Add safety checks for all updatePluginTabStates calls
- Add safety check in Alpine component tab button handler
- Add safety check in Alpine callback
- Prevents TypeError when function is not available in all contexts
* fix(ui): Add safety check in Alpine callback for updatePluginTabStates
* debug(ui): Add console logging to trace on-demand modal opening
- Add logging to runPluginOnDemand function
- Add logging to __openOnDemandModalImpl function
- Log plugin lookup, modal element checks, and display changes
- Helps diagnose why modal doesn't open when button is clicked
* debug(ui): Add logging for modal display change
* debug(ui): Add more explicit modal visibility settings and computed style logging
- Set visibility and opacity explicitly when showing modal
- Force reflow to ensure styles are applied
- Log computed styles to diagnose CSS issues
- Helps identify if modal is hidden by CSS rules
* debug(ui): Increase modal z-index and add bounding rect check
- Set z-index to 9999 to ensure modal is above all other elements
- Add bounding rect check to verify modal is in viewport
- Helps diagnose if modal is positioned off-screen or behind other elements
* debug(display): Add detailed logging for on-demand restart flow
- Log when polling finds requests
- Log service detection result
- Log file writing and systemctl commands
- Log restart command execution and results
- Helps diagnose why on-demand restart isn't working
* debug(display): Add logging for on-demand request polling
- Log request_id comparison to diagnose why requests aren't being processed
- Helps identify if request_id matching is preventing processing
* fix(ui): Force modal positioning with !important to override any conflicting styles
- Use cssText with !important flags to ensure modal is always visible
- Remove all inline styles first to start fresh
- Ensure modal is positioned at top:0, left:0 with fixed positioning
- Fixes issue where modal was still positioned off-screen (top: 2422px)
* debug(ui): Add logging to on-demand form submission
- Log form submission events
- Log payload being sent
- Log response status and data
- Helps diagnose why on-demand requests aren't being processed
* fix(display): Remove restart-based on-demand activation
- Replace restart-based activation with immediate mode switch
- On-demand now activates without restarting the service
- Saves rotation state for restoration when on-demand ends
- Fixes infinite restart loop issue
- On-demand now works when display is already running
* docs: Add comprehensive guide for on-demand cache management
- Document all on-demand cache keys and their purposes
- Explain when manual clearing is needed
- Clarify what clearing from cache management tab does/doesn't do
- Provide troubleshooting steps and best practices
* fix(display): Ensure on-demand takes priority over live priority
- Move on-demand check BEFORE live priority check
- Add explicit logging when on-demand overrides live priority
- Improve request_id checking with both instance and persisted checks
- Add debug logging to trace why requests aren't being processed
- Fixes issue where on-demand didn't interrupt live NHL game
* fix(display): Ensure on-demand takes priority over live priority
- Move on-demand check BEFORE live priority check in main loop
- Add explicit logging when on-demand overrides live priority
- Fixes issue where on-demand didn't interrupt live NHL game
* fix(display): Improve on-demand request processing and priority
- Add persistent processed_id check to prevent duplicate processing
- Mark request as processed BEFORE processing to prevent race conditions
- Improve logging to trace request processing
- Ensure on-demand takes priority over live priority (already fixed in previous commit)
* fix(display): Remove duplicate action line
* fix(display): Fix live priority and ensure on-demand overrides it
- Fix live priority to properly set active_mode when live content is detected
- Ensure on-demand check happens before live priority check
- Add debug logging to trace on-demand vs live priority
- Fix live priority to stay on live mode instead of rotating
* fix(display): Add debug logging for on-demand priority check
* fix(display): Add better logging for on-demand request processing
- Add logging to show when requests are blocked by processed_id check
- Add logging to show on-demand state after activation
- Helps debug why on-demand requests aren't being processed
* fix(display): Add detailed logging for on-demand activation and checking
- Log on-demand state after activation to verify it's set correctly
- Add debug logging in main loop to trace on-demand check
- Helps identify why on-demand isn't overriding live priority
* fix(display): Add debug logging for on-demand check in main loop
* fix(display): Remove restart logic from _clear_on_demand and fix cache delete
- Replace cache_manager.delete() with cache_manager.clear_cache()
- Remove restart logic from _clear_on_demand - now clears immediately
- Restore rotation state immediately without restarting
- Fixes AttributeError: 'CacheManager' object has no attribute 'delete'
* fix(display): Remove restart logic from _clear_on_demand
- Remove restart logic - now clears on-demand state immediately
- Restore rotation state immediately without restarting
- Use clear_cache instead of delete (already fixed in previous commit)
- Fixes error when stopping on-demand mode
* feat(display): Clear display before activating on-demand mode
- Clear display and reset state before activating on-demand
- Reset dynamic mode state to ensure clean transition
- Mimics the behavior of manually stopping display first
- Should fix issue where on-demand only works after manual stop
* feat(display): Stop display service before starting on-demand mode
- Stop the display service first if it's running
- Wait 1.5 seconds for clean shutdown
- Then start the service with on-demand request in cache
- Mimics the manual workflow of stopping display first
- Should fix issue where on-demand only works after manual stop
* feat(display): Filter plugins during initialization for on-demand mode
- Check cache for on-demand requests during initialization
- Only load the on-demand plugin if on-demand request is found
- Prevents loading background services for other plugins
- Fixes issue where Hockey/Football data loads even when only Clock is requested
* fix(display): Use filtered enabled_plugins list instead of discovered_plugins
- Use enabled_plugins list which is already filtered for on-demand mode
- Prevents loading all plugins when on-demand mode is active
- Fixes issue where all plugins were loaded even in on-demand mode
* fix(display): Fix on-demand stop request processing and expiration check
- Always process stop requests, even if request_id was seen before
- Fix expiration check to handle cases where on-demand is not active
- Add better logging for stop requests and expiration
- Fixes issue where stop button does nothing and timer doesn't expire
* fix(display): Fix on-demand stop processing, expiration, and plugin filtering
- Fix stop request processing to always process stop requests, bypassing request_id checks
- Fix expiration check logic to properly check on_demand_active and expires_at separately
- Store display_on_demand_config cache key in _activate_on_demand for plugin filtering
- Clear display before switching to on-demand mode to prevent visual artifacts
- Clear display_on_demand_config cache key in _clear_on_demand to prevent stale data
- Implement plugin filtering during initialization based on display_on_demand_config
Fixes issues where:
- Stop button did nothing (stop requests were blocked by request_id check)
- Expiration timer didn't work (logic issue with or condition)
- Plugin filtering didn't work on restart (config cache key never set)
- Display showed artifacts when switching to on-demand (display not cleared)
- All plugins loaded even in on-demand mode (filtering not implemented)
* fix(web): Allow on-demand to work with disabled plugins
- Remove frontend checks that blocked disabled plugins from on-demand
- Backend already supports temporarily enabling disabled plugins during on-demand
- Update UI messages to indicate plugin will be temporarily enabled
- Remove disabled attribute from Run On-Demand button
Fixes issue where disabled plugins couldn't use on-demand feature even
though the backend implementation supports it.
* fix(display): Resolve plugin_id when sent as mode in on-demand requests
- Detect when mode parameter is actually a plugin_id and resolve to first display mode
- Handle case where frontend sends plugin_id as mode (e.g., 'football-scoreboard')
- Add fallback to use first available display mode if provided mode is invalid
- Add logging for mode resolution debugging
Fixes issue where on-demand requests with mode=plugin_id failed with 'invalid-mode' error
* feat(display): Rotate through all plugin modes in on-demand mode
- Store all modes for on-demand plugin instead of locking to single mode
- Rotate through available modes (live, recent, upcoming) when on-demand active
- Skip modes that return False (no content) and move to next mode
- Prioritize live modes if they have content, otherwise skip them
- Add on_demand_modes list and on_demand_mode_index for rotation tracking
Fixes issue where on-demand mode stayed on one mode (e.g., football_recent)
and didn't rotate through other available modes (football_live, football_upcoming).
Now properly rotates through all modes, skipping empty ones.
* fix(display): Improve on-demand stop request handling
- Always process stop requests if on-demand is active, even if same request_id
- Add better logging when stop is requested but on-demand is not active
- Improve logging in _clear_on_demand to show which mode rotation resumes to
- Ensure stop requests are properly acknowledged
Fixes issue where stop button shows as completed but display doesn't resume
normal rotation. Stop requests now properly clear on-demand state and resume.
* security(web): Fix XSS vulnerability in GitHub auth error display
Replace innerHTML usage with safe DOM manipulation:
- Use textContent to clear element and create text nodes
- Create <strong> element via createElement instead of string HTML
- Add safe fallback ('Unknown error') for error messages
- Ensure authData.error/authData.message are treated as plain text
- Avoid trusting backend-provided data as HTML
Fixes XSS vulnerability where malicious HTML in error messages could
be injected into the DOM.
* style(api): Remove unnecessary str() in f-string for error message
Remove explicit str(e) call in error_response f-string since f-strings
automatically convert exceptions to strings. This matches the style used
elsewhere in the file.
Changed: f"Error saving configuration: {str(e)}"
To: f"Error saving configuration: {e}"
* fix(store): Skip caching for rate-limited 403 responses
When a 403 response indicates a rate limit (detected by checking if
'rate limit' is in response.text.lower()), return the error result but
do NOT cache it in _token_validation_cache. Rate limits are temporary
and should be retried, so caching would incorrectly mark the token as
invalid.
Continue to cache 403 responses that indicate missing token permissions,
as these are persistent issues that should be cached.
This prevents rate-limited responses from being incorrectly cached as
invalid tokens, allowing the system to retry after the rate limit
resets.
* fix(display): Prevent ZeroDivisionError when on_demand_modes is empty
Add guards to check if on_demand_modes is non-empty before performing
any rotation/index math operations. When on_demand_active is True but
on_demand_modes is empty, clear on-demand mode instead of attempting
division by zero.
Fixed in three locations:
1. Mode selection logic (line ~1081): Check before accessing modes
2. Skip to next mode when no content (line ~1190): Guard before modulo
3. Rotate to next mode (line ~1561): Guard before modulo
This prevents ZeroDivisionError when a plugin has no available display
modes or when on_demand_modes becomes empty unexpectedly.
* fix(display): Improve guard for empty on_demand_modes in rotation skip
Refine the guard around lines 1195-1209 to:
- Check if on_demand_modes is empty before any modulo/index operations
- Log warning and debug trace when no modes are configured
- Skip rotation (continue) instead of clearing on-demand mode
- Only perform modulo and index operations when modes are available
- Only log rotation message when next_mode is valid
This prevents ZeroDivisionError and ensures all logging only occurs
when next_mode is valid, providing better traceability.
* fix(display): Populate on_demand_modes when restoring on-demand state from cache
When restoring on-demand state from cache during initialization (around
lines 163-197), the code sets on_demand_active, on_demand_plugin_id and
related fields but does not populate self.on_demand_modes, causing the
run loop to see an empty modes list after restart.
Fix by:
1. Adding _populate_on_demand_modes_from_plugin() method that retrieves
the plugin's display modes from plugin_display_modes and builds the
ordered modes list (prioritizing live modes with content, same logic
as _activate_on_demand)
2. Calling this method after plugin loading completes (around line 296)
when on_demand_active and on_demand_plugin_id are set
3. Setting on_demand_mode_index to match the restored mode if available,
otherwise starting at index 0
This ensures on_demand_modes is populated after restart, preventing
empty modes list errors in the run loop.
* docs: Update on-demand documentation to reflect current implementation
Replace obsolete log message reference with current log messages:
- Old: 'Activating on-demand mode... restarting display controller'
- New: 'Processing on-demand start request for plugin' and 'Activated on-demand for plugin'
Update Scenario 2 to reflect immediate mode switching:
- Changed title from 'Infinite Restart Loop' to 'On-Demand Mode Switching Issues'
- Updated symptoms to describe mode switching issues instead of restart loops
- Added note that on-demand now switches modes immediately without restarting
- Updated solution to include display_on_demand_state key
This reflects the current implementation where on-demand activates
immediately without restarting the service.
* fix(api): Fix undefined logger and service stop logic in start_on_demand_display
- Add module-level logger to avoid NameError when logging disabled plugin
- Only stop display service when start_service is True (prevents stopping
service without restarting when start_service is False)
- Remove unused stop_result variable
- Clean up f-strings that don't need formatting
- Improve code formatting for logger.info call
Fixes issue where logger.info() would raise NameError and where the
service would be stopped even when start_service=False, leaving the
service stopped without restarting it.
---------
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Co-authored-by: Chuck <chuck@example.com>
* fix(plugins): Resolve plugin ID determination error in action buttons
- Fix server-side template parameter order for executePluginAction
- Add data-plugin-id attributes to action buttons in all templates
- Enhance executePluginAction with comprehensive fallback logic
- Support retrieving pluginId from DOM, Alpine context, and config state
- Fixes 'Unable to determine plugin ID' error for Spotify/YouTube auth
* fix(plugins): Add missing button IDs and status divs in server-side action template
- Add action-{id}-{index} IDs to action buttons
- Add action-status-{id}-{index} status divs for each action
- Match client-side template structure for consistency
- Fixes 'Action elements not found' error
* fix(api): Fix indentation error in execute_plugin_action function
- Fix incorrect else block indentation that caused 500 errors
- Correct indentation for OAuth flow and simple script execution paths
- Resolves syntax error preventing plugin actions from executing
* fix(api): Improve error handling for plugin actions and config saves
- Add better JSON parsing error handling with request details
- Add detailed permission error messages for secrets file saves
- Include file path and permission status in error responses
- Helps diagnose 400 errors on action execution and 500 errors on config saves
* fix(api): Add detailed permission error handling for secrets config saves
- Add PermissionError-specific handling with permission checks
- Include directory and file permission status in error logs
- Provide more helpful error messages with file paths
- Helps diagnose permission issues when saving config_secrets.json
* fix(config): Add permission check and actionable error message for config saves
- Check file writability before attempting write
- Show file owner and current permissions in error message
- Provide exact command to fix permissions (chown + chmod)
- Helps diagnose and resolve permission issues with config_secrets.json
* fix(config): Preserve detailed permission error messages
- Handle PermissionError separately to preserve detailed error messages
- Ensure actionable permission fix commands are included in error response
- Prevents detailed error messages from being lost in exception chain
* fix(config): Remove overly strict pre-write permission check
- Remove pre-write file existence/writability check that was blocking valid writes
- Let actual file write operation determine success/failure
- Provide detailed error messages only when write actually fails
- Fixes regression where config_secrets.json saves were blocked unnecessarily
* fix(config): Use atomic writes for config_secrets.json to handle permission issues
- Write to temp file first, then atomically move to final location
- Works even when existing file isn't writable (as long as directory is writable)
- Matches pattern used elsewhere in codebase (disk_cache, atomic_manager)
- Fixes permission errors when saving secrets configuration
* chore: Update music plugin submodule to include live_priority fix
* fix(plugins): Improve plugin ID determination in dynamic button generation
- Update generateFormFromSchema to pass currentPluginConfig?.pluginId and add data attributes
- Update generateSimpleConfigForm to pass currentPluginConfig?.pluginId and add data attributes
- Scope fallback 6 DOM lookup to button context instead of document-wide search
- Ensures correct plugin tab selection when multiple plugins are present
- Maintains existing try/catch error handling and logging
* chore: Update music plugin submodule to fix has_live_priority enabled attribute
* chore: Update music plugin submodule - remove redundant music_priority_mode
* fix(web-ui): Fix file upload widget detection for nested plugin properties
- Added helper function to get schema properties by full key path
- Enhanced x-widget detection to check both property object and schema directly
- Improved upload config retrieval with fallback to schema
- Added debug logging for file-upload widget detection
- Fixes issue where static-image plugin file upload widget was not rendering
The file upload widget was not being detected for nested properties like
image_config.images because the x-widget attribute wasn't being checked
in the schema directly. This fix ensures the widget is properly detected
and rendered even when nested deep in the configuration structure.
* fix(web-ui): Improve file upload widget detection with direct schema fallback
- Fixed getSchemaProperty helper function to correctly navigate nested paths
- Added direct schema lookup fallback for image_config.images path
- Enhanced debug logging to diagnose widget detection issues
- Simplified widget detection logic while maintaining robustness
* fix(web-ui): Add aggressive schema lookup for file-upload widget detection
- Always try direct schema navigation for image_config.images
- Added general direct lookup fallback if getSchemaProperty fails
- Enhanced debug logging with schema existence checks
- Prioritize schema lookup over prop object for x-widget detection
* fix(web-ui): Add direct check for top-level images field in file upload detection
- Added specific check for top-level 'images' field (flattened schema)
- Enhanced debug logging to show all x-widget detection attempts
- Improved widget detection to check prop object more thoroughly
* fix(web-ui): Prioritize prop object for x-widget detection
- Check prop object first (should have x-widget from schema)
- Then fall back to schema lookup
- Enhanced debug logging to show all detection attempts
* fix(web-ui): Add aggressive direct detection for images file upload widget
- Added direct check for 'images' field in schema.properties.images
- Multiple fallback detection methods (direct, prop object, schema lookup)
- Simplified logic to explicitly check for file-upload widget
- Enhanced debug logging to show detection path
* fix(web-ui): Add file upload widget support to server-side Jinja2 template
- Added check for x-widget: file-upload in array field rendering
- Renders file upload drop zone with drag-and-drop support
- Displays uploaded images list with delete and schedule buttons
- Falls back to comma-separated text input for regular arrays
- Fixes file upload widget not appearing in static-image plugin
* feat(web-ui): Add route to serve plugin asset files from assets directory
- Added /assets/plugins/<plugin_id>/uploads/<filename> route
- Serves uploaded images and other assets with proper content types
- Includes security checks to prevent directory traversal
- Fixes 404 errors when displaying uploaded plugin images
* fix(web-ui): Fix import for send_from_directory in plugin assets route
* feat(web-ui): Load uploaded images from metadata file when rendering config form
- Populates images field from .metadata.json if not in config
- Ensures uploaded images appear in form even before config is saved
- Merges metadata images with existing config images to avoid duplicates
* fix(web-ui): Fix PROJECT_ROOT reference in image metadata loading
* docs(web-ui): Add reminder to save configuration after file upload
- Added informational note below upload widget
- Reminds users to save config after uploading files
- Uses amber color and info icon for visibility
* fix(web-ui): Move plugin asset serving route to main app
- Moved /assets/plugins/... route from api_v3 blueprint to main app
- Blueprint has /api/v3 prefix, but route needs to be at /assets/...
- Fixes 404 errors when trying to display uploaded images
- Route must be on main app for correct URL path
* security(web-ui): Fix path containment check in plugin asset serving
- Replace string startswith() with proper path resolution using os.path.commonpath()
- Prevents prefix-based directory traversal bypasses
- Uses resolved absolute paths to ensure true path containment
- Handles ValueError for cross-drive paths (Windows compatibility)
* security(web-ui): Remove traceback exposure from plugin asset serving errors
- Return generic error message instead of full traceback in production
- Log exceptions server-side using app.logger.exception()
- Only include detailed error information when app.debug is True
- Prevents leaking internal implementation details to clients
* fix(web-ui): Assign currentPluginConfig to window for template access
- Assign currentPluginConfig to window.currentPluginConfig when building the object
- Fixes empty pluginId in template interpolation for plugin action buttons
- Ensures window.currentPluginConfig?.pluginId is available in onclick handlers
- Prevents executePluginAction from receiving empty pluginId parameter
* chore: Update music plugin submodule to include .gitignore
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(plugins): Resolve plugin ID determination error in action buttons
- Fix server-side template parameter order for executePluginAction
- Add data-plugin-id attributes to action buttons in all templates
- Enhance executePluginAction with comprehensive fallback logic
- Support retrieving pluginId from DOM, Alpine context, and config state
- Fixes 'Unable to determine plugin ID' error for Spotify/YouTube auth
* fix(plugins): Add missing button IDs and status divs in server-side action template
- Add action-{id}-{index} IDs to action buttons
- Add action-status-{id}-{index} status divs for each action
- Match client-side template structure for consistency
- Fixes 'Action elements not found' error
* fix(api): Fix indentation error in execute_plugin_action function
- Fix incorrect else block indentation that caused 500 errors
- Correct indentation for OAuth flow and simple script execution paths
- Resolves syntax error preventing plugin actions from executing
* fix(api): Improve error handling for plugin actions and config saves
- Add better JSON parsing error handling with request details
- Add detailed permission error messages for secrets file saves
- Include file path and permission status in error responses
- Helps diagnose 400 errors on action execution and 500 errors on config saves
* fix(api): Add detailed permission error handling for secrets config saves
- Add PermissionError-specific handling with permission checks
- Include directory and file permission status in error logs
- Provide more helpful error messages with file paths
- Helps diagnose permission issues when saving config_secrets.json
* fix(config): Add permission check and actionable error message for config saves
- Check file writability before attempting write
- Show file owner and current permissions in error message
- Provide exact command to fix permissions (chown + chmod)
- Helps diagnose and resolve permission issues with config_secrets.json
* fix(config): Preserve detailed permission error messages
- Handle PermissionError separately to preserve detailed error messages
- Ensure actionable permission fix commands are included in error response
- Prevents detailed error messages from being lost in exception chain
* fix(config): Remove overly strict pre-write permission check
- Remove pre-write file existence/writability check that was blocking valid writes
- Let actual file write operation determine success/failure
- Provide detailed error messages only when write actually fails
- Fixes regression where config_secrets.json saves were blocked unnecessarily
* fix(config): Use atomic writes for config_secrets.json to handle permission issues
- Write to temp file first, then atomically move to final location
- Works even when existing file isn't writable (as long as directory is writable)
- Matches pattern used elsewhere in codebase (disk_cache, atomic_manager)
- Fixes permission errors when saving secrets configuration
* chore: Update music plugin submodule to include live_priority fix
* fix(plugins): Improve plugin ID determination in dynamic button generation
- Update generateFormFromSchema to pass currentPluginConfig?.pluginId and add data attributes
- Update generateSimpleConfigForm to pass currentPluginConfig?.pluginId and add data attributes
- Scope fallback 6 DOM lookup to button context instead of document-wide search
- Ensures correct plugin tab selection when multiple plugins are present
- Maintains existing try/catch error handling and logging
---------
Co-authored-by: Chuck <chuck@example.com>
- Removed cozette_bdf from common_fonts dictionary in font_manager.py
- Removed cozette_bdf option from web interface font selectors
- Resolves warning: 'Common font file not found: assets/fonts/cozette.bdf'
- Font can be re-enabled by adding the font file and re-adding to common_fonts
Co-authored-by: Chuck <chuck@example.com>
* fix(web): Resolve font display and config API error handling issues
- Fix font catalog display error where path.startsWith fails
(path is object, not string)
- Update save_main_config to use error_response() helper
- Improve save_raw_main_config error handling consistency
- Add proper error codes and traceback details to API responses
* fix(web): Prevent fontCatalog redeclaration error on HTMX reload
- Use window object to store global font variables
- Check if script has already loaded before declaring variables
- Update both window properties and local references on assignment
- Fixes 'Identifier fontCatalog has already been declared' error
* fix(web): Wrap fonts script in IIFE to prevent all redeclaration errors
- Wrap entire script in IIFE that only runs once
- Check if script already loaded before declaring variables/functions
- Expose initializeFontsTab to window for re-initialization
- Prevents 'Identifier has already been declared' errors on HTMX reload
* fix(web): Exempt config save API endpoints from CSRF protection
- Exempt save_raw_main_config, save_raw_secrets_config, and save_main_config from CSRF
- These endpoints are called via fetch from JavaScript and don't include CSRF tokens
- Fixes 500 error when saving config via raw JSON editor
* fix(web): Exempt system action endpoint from CSRF protection
- Exempt execute_system_action from CSRF
- Fixes 500 error when using system action buttons (restart display, restart Pi, etc.)
- These endpoints are called via HTMX and don't include CSRF tokens
* fix(web): Exempt all API v3 endpoints from CSRF protection
- Add before_request handler to exempt all api_v3.* endpoints
- All API endpoints are programmatic (HTMX/fetch) and don't include CSRF tokens
- Prevents future CSRF errors on any API endpoint
- Cleaner than exempting individual endpoints
* refactor(web): Remove CSRF protection for local-only application
- CSRF is designed for internet-facing apps to prevent cross-site attacks
- For local-only Raspberry Pi app, threat model is different
- All endpoints were exempted anyway, so it wasn't protecting anything
- Forms use HTMX without CSRF tokens
- If exposing to internet later, can re-enable with proper token implementation
* fix(web): Fix font path double-prefixing in font catalog display
- Only prefix with 'assets/fonts/' if path is a bare filename
- If path starts with '/' (absolute) or 'assets/' (already prefixed), use as-is
- Fixes double-prefixing when get_fonts_catalog returns relative paths like 'assets/fonts/press_start.ttf'
* fix(web): Remove fontsTabInitialized guard to allow re-initialization on HTMX reload
- Remove fontsTabInitialized check that prevented re-initialization on HTMX content swap
- The window._fontsScriptLoaded guard is sufficient to prevent function redeclaration
- Allow initializeFontsTab() to run on each HTMX swap to attach listeners to new DOM elements
- Fixes fonts UI breaking after HTMX reload (buttons, upload dropzone, etc. not working)
* fix(api): Preserve empty strings for optional string fields in plugin config
- Add _is_field_required() helper to check if fields are required in schema
- Update _parse_form_value_with_schema() to preserve empty strings for optional string fields
- Fixes 400 error when saving MQTT plugin config with empty username/password
- Resolves validation error: 'Expected type string, got NoneType'
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(web): Resolve font display and config API error handling issues
- Fix font catalog display error where path.startsWith fails
(path is object, not string)
- Update save_main_config to use error_response() helper
- Improve save_raw_main_config error handling consistency
- Add proper error codes and traceback details to API responses
* fix(web): Prevent fontCatalog redeclaration error on HTMX reload
- Use window object to store global font variables
- Check if script has already loaded before declaring variables
- Update both window properties and local references on assignment
- Fixes 'Identifier fontCatalog has already been declared' error
* fix(web): Wrap fonts script in IIFE to prevent all redeclaration errors
- Wrap entire script in IIFE that only runs once
- Check if script already loaded before declaring variables/functions
- Expose initializeFontsTab to window for re-initialization
- Prevents 'Identifier has already been declared' errors on HTMX reload
* fix(web): Exempt config save API endpoints from CSRF protection
- Exempt save_raw_main_config, save_raw_secrets_config, and save_main_config from CSRF
- These endpoints are called via fetch from JavaScript and don't include CSRF tokens
- Fixes 500 error when saving config via raw JSON editor
* fix(web): Exempt system action endpoint from CSRF protection
- Exempt execute_system_action from CSRF
- Fixes 500 error when using system action buttons (restart display, restart Pi, etc.)
- These endpoints are called via HTMX and don't include CSRF tokens
* fix(web): Exempt all API v3 endpoints from CSRF protection
- Add before_request handler to exempt all api_v3.* endpoints
- All API endpoints are programmatic (HTMX/fetch) and don't include CSRF tokens
- Prevents future CSRF errors on any API endpoint
- Cleaner than exempting individual endpoints
* refactor(web): Remove CSRF protection for local-only application
- CSRF is designed for internet-facing apps to prevent cross-site attacks
- For local-only Raspberry Pi app, threat model is different
- All endpoints were exempted anyway, so it wasn't protecting anything
- Forms use HTMX without CSRF tokens
- If exposing to internet later, can re-enable with proper token implementation
* fix(web): Fix font path double-prefixing in font catalog display
- Only prefix with 'assets/fonts/' if path is a bare filename
- If path starts with '/' (absolute) or 'assets/' (already prefixed), use as-is
- Fixes double-prefixing when get_fonts_catalog returns relative paths like 'assets/fonts/press_start.ttf'
* fix(web): Remove fontsTabInitialized guard to allow re-initialization on HTMX reload
- Remove fontsTabInitialized check that prevented re-initialization on HTMX content swap
- The window._fontsScriptLoaded guard is sufficient to prevent function redeclaration
- Allow initializeFontsTab() to run on each HTMX swap to attach listeners to new DOM elements
- Fixes fonts UI breaking after HTMX reload (buttons, upload dropzone, etc. not working)
* fix(api): Preserve empty strings for optional string fields in plugin config
- Add _is_field_required() helper to check if fields are required in schema
- Update _parse_form_value_with_schema() to preserve empty strings for optional string fields
- Fixes 400 error when saving MQTT plugin config with empty username/password
- Resolves validation error: 'Expected type string, got NoneType'
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(web): Resolve font display and config API error handling issues
- Fix font catalog display error where path.startsWith fails
(path is object, not string)
- Update save_main_config to use error_response() helper
- Improve save_raw_main_config error handling consistency
- Add proper error codes and traceback details to API responses
* fix(web): Prevent fontCatalog redeclaration error on HTMX reload
- Use window object to store global font variables
- Check if script has already loaded before declaring variables
- Update both window properties and local references on assignment
- Fixes 'Identifier fontCatalog has already been declared' error
* fix(web): Wrap fonts script in IIFE to prevent all redeclaration errors
- Wrap entire script in IIFE that only runs once
- Check if script already loaded before declaring variables/functions
- Expose initializeFontsTab to window for re-initialization
- Prevents 'Identifier has already been declared' errors on HTMX reload
* fix(web): Exempt config save API endpoints from CSRF protection
- Exempt save_raw_main_config, save_raw_secrets_config, and save_main_config from CSRF
- These endpoints are called via fetch from JavaScript and don't include CSRF tokens
- Fixes 500 error when saving config via raw JSON editor
* fix(web): Exempt system action endpoint from CSRF protection
- Exempt execute_system_action from CSRF
- Fixes 500 error when using system action buttons (restart display, restart Pi, etc.)
- These endpoints are called via HTMX and don't include CSRF tokens
* fix(web): Exempt all API v3 endpoints from CSRF protection
- Add before_request handler to exempt all api_v3.* endpoints
- All API endpoints are programmatic (HTMX/fetch) and don't include CSRF tokens
- Prevents future CSRF errors on any API endpoint
- Cleaner than exempting individual endpoints
* refactor(web): Remove CSRF protection for local-only application
- CSRF is designed for internet-facing apps to prevent cross-site attacks
- For local-only Raspberry Pi app, threat model is different
- All endpoints were exempted anyway, so it wasn't protecting anything
- Forms use HTMX without CSRF tokens
- If exposing to internet later, can re-enable with proper token implementation
* fix(web): Fix font path double-prefixing in font catalog display
- Only prefix with 'assets/fonts/' if path is a bare filename
- If path starts with '/' (absolute) or 'assets/' (already prefixed), use as-is
- Fixes double-prefixing when get_fonts_catalog returns relative paths like 'assets/fonts/press_start.ttf'
* fix(web): Remove fontsTabInitialized guard to allow re-initialization on HTMX reload
- Remove fontsTabInitialized check that prevented re-initialization on HTMX content swap
- The window._fontsScriptLoaded guard is sufficient to prevent function redeclaration
- Allow initializeFontsTab() to run on each HTMX swap to attach listeners to new DOM elements
- Fixes fonts UI breaking after HTMX reload (buttons, upload dropzone, etc. not working)
* fix(api): Preserve empty strings for optional string fields in plugin config
- Add _is_field_required() helper to check if fields are required in schema
- Update _parse_form_value_with_schema() to preserve empty strings for optional string fields
- Fixes 400 error when saving MQTT plugin config with empty username/password
- Resolves validation error: 'Expected type string, got NoneType'
* fix(config): Add defaults to schemas and fix None value handling
- Updated merge_with_defaults to replace None values with defaults
- Fixed form processing to skip empty optional fields without defaults
- Added script to automatically add defaults to all plugin config schemas
- Added defaults to 89 fields across 10 plugin schemas
- Prevents validation errors from None values in configs
Changes:
- schema_manager.py: Enhanced merge_with_defaults to replace None with defaults
- api_v3.py: Added _SKIP_FIELD sentinel to skip optional fields without defaults
- add_defaults_to_schemas.py: Script to add sensible defaults to schemas
- Plugin schemas: Added defaults for number, boolean, and array fields
* fix(config): Fix save button spinner by checking HTTP status code
- Fixed handleConfigSave to check xhr.status instead of event.detail.successful
- With hx-swap="none", HTMX doesn't set event.detail.successful
- Now properly detects successful saves (status 200-299) and stops spinner
- Improved error message extraction from API responses
- Also fixed handleToggleResponse for consistency
* fix(web-ui): Resolve GitHub token warning persistence after save
- Made checkGitHubAuthStatus() return Promise for proper async handling
- Clear sessionStorage dismissal flag when token is saved
- Add delay before status check to ensure backend token reload
- Wait for status check completion before hiding settings panel
Fixes issue where GitHub token warnings and pop-ups would not
disappear after successfully saving a token in the web UI.
* fix(web-ui): Add token validation and improve GitHub token warning behavior
- Add token validation to backend API endpoint to check if token is valid/expired
- Implement _validate_github_token() method in PluginStoreManager with caching
- Update frontend to show warning only when token is missing or invalid
- Keep settings panel accessible (collapsible) when token is configured
- Collapse settings panel content after successful token save instead of hiding
- Display specific error messages for invalid/expired tokens
- Clear sessionStorage dismissal flag when token becomes valid
Fixes issue where GitHub token warnings and settings panel would not
properly hide/show based on token status. Now validates token validity
and provides better UX with collapsible settings panel.
* fix(web-ui): Fix CSS/display issue for GitHub token warning and settings
- Update all hide/show operations to use both classList and style.display
- Fix checkGitHubAuthStatus() to properly hide/show warning and settings
- Fix dismissGithubWarning() to use both methods
- Fix toggleGithubTokenSettings() with improved state checking
- Fix collapse button handler with improved state checking
- Fix saveGithubToken() to properly show/collapse settings panel
This ensures elements actually hide/show when status changes, matching
the pattern used elsewhere in the codebase (like toggleSection). All
buttons (dismiss, close, collapse) should now work correctly.
* fix(web-ui): Fix GitHub token expand button functionality
- Convert collapse button handler to named function (toggleGithubTokenContent)
- Improve state checking using class, inline style, and computed style
- Re-attach event listener after saving token to ensure it works
- Add console logging for debugging
- Make function globally accessible for better reliability
Fixes issue where expand button didn't work after saving token.
* fix(web-ui): Remove X button and improve GitHub token panel behavior
- Remove X (close) button from GitHub token configuration panel
- Replace toggleGithubTokenSettings() with openGithubTokenSettings() that only opens
- Auto-collapse panel when token is valid (user must click expand to edit)
- Auto-detect token status on page load (no need to click save)
- Simplify saveGithubToken() to rely on checkGitHubAuthStatus() for UI updates
- Ensure expand button works correctly with proper event listener attachment
The panel now remains visible but collapsed when a token is configured,
allowing users to expand it when needed without the ability to completely hide it.
* refactor(web-ui): Improve GitHub token collapse button code quality
- Update comment to reflect actual behavior (prevent parent click handlers)
- Use empty string for display to defer to CSS instead of hard-coding block/none
- Extract duplicate clone-and-attach logic into attachGithubTokenCollapseHandler() helper
- Make helper function globally accessible for reuse in checkGitHubAuthStatus()
Improves maintainability and makes code more future-proof for layout changes.
* fix(web-ui): Fix collapse/expand button by using removeProperty for display
- Use style.removeProperty('display') instead of style.display = ''
- This properly removes inline styles and defers to CSS classes
- Fixes issue where collapse/expand button stopped working after refactor
* fix(web-ui): Make display handling consistent for token collapse
- Use removeProperty('display') consistently in all places
- Fix checkGitHubAuthStatus() to use removeProperty instead of inline style
- Simplify state checking to rely on hidden class with computed style fallback
- Ensures collapse/expand button works correctly by deferring to CSS classes
* fix(web-ui): Fix token collapse button and simplify state detection
- Simplify state checking to rely on hidden class only (element has class='block')
- Only remove inline display style if it exists (check before removing)
- Add console logging to debug handler attachment
- Ensure collapse/expand works by relying on CSS classes
Fixes issues where:
- Collapse button did nothing
- Auto-detection of token status wasn't working
* debug(web-ui): Add extensive debugging for token collapse button
- Add console logs to track function calls and element detection
- Improve state detection to use computed style as fallback
- Add wrapper function for click handler to ensure it's called
- Better error messages to identify why handler might not attach
This will help identify why the collapse button isn't working.
* debug(web-ui): Add comprehensive debugging for GitHub token features
- Add console logs to checkGitHubAuthStatus() to track execution
- Re-attach collapse handler after plugin store is rendered
- Add error stack traces for better debugging
- Ensure handler is attached when content is dynamically loaded
This will help identify why:
- Auto-detection of token status isn't working
- Collapse button isn't functioning
* fix(web-ui): Move checkGitHubAuthStatus before IIFE to fix scope issue
- Move checkGitHubAuthStatus function definition before IIFE starts
- Function was defined after IIFE but called inside it, causing it to be undefined
- Now function is available when called during initialization
- This should fix auto-detection of token status on page load
* debug(web-ui): Add extensive logging to GitHub token functions
- Add logging when checkGitHubAuthStatus is defined
- Add logging when function is called during initialization
- Add logging in attachGithubTokenCollapseHandler
- Add logging in store render callback
- This will help identify why functions aren't executing
* fix(web-ui): Move GitHub token functions outside IIFE for availability
- Move attachGithubTokenCollapseHandler and toggleGithubTokenContent outside IIFE
- These functions need to be available when store renders, before IIFE completes
- Add logging to initializePlugins to track when it's called
- This should fix the 'undefined' error when store tries to attach handlers
* fix(web-ui): Fix GitHub token content collapse/expand functionality
- Element has 'block' class in HTML which conflicts with 'hidden' class
- When hiding: add 'hidden', remove 'block', set display:none inline
- When showing: remove 'hidden', add 'block', remove inline display
- This ensures proper visibility toggle for the GitHub API Configuration section
* perf(web-ui): Optimize GitHub token detection speed
- Call checkGitHubAuthStatus immediately when script loads (if elements exist)
- Call it early in initPluginsPage (before full initialization completes)
- Use requestAnimationFrame instead of setTimeout(100ms) for store render callback
- Reduce save token delay from 300ms to 100ms
- Token detection now happens in parallel with other initialization tasks
- This makes token status visible much faster on page load
* fix(web-ui): Fix all collapse/expand buttons on plugins page
- Fix Installed Plugins section collapse/expand button
- Fix Plugin Store section collapse/expand button
- Fix GitHub Install section collapse/expand button
- Apply same fixes as GitHub token button:
* Clone buttons to remove existing listeners
* Handle block/hidden class conflicts properly
* Add proper event prevention (stopPropagation/preventDefault)
* Add logging for debugging
- All collapse/expand buttons should now work correctly
* fix(web-ui): Fix syntax error in setupGitHubInstallHandlers
- Ensure all handler setup code is inside the function
- Add comment to mark function end clearly
* refactor(web-ui): Remove collapse buttons from Installed Plugins and Plugin Store
- Remove collapse/expand buttons from Installed Plugins section
- Remove collapse/expand buttons from Plugin Store section
- Remove related JavaScript handler code
- These sections are now always visible for better UX
- GitHub token section still has collapse functionality
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(web): Resolve font display and config API error handling issues
- Fix font catalog display error where path.startsWith fails
(path is object, not string)
- Update save_main_config to use error_response() helper
- Improve save_raw_main_config error handling consistency
- Add proper error codes and traceback details to API responses
* fix(web): Prevent fontCatalog redeclaration error on HTMX reload
- Use window object to store global font variables
- Check if script has already loaded before declaring variables
- Update both window properties and local references on assignment
- Fixes 'Identifier fontCatalog has already been declared' error
* fix(web): Wrap fonts script in IIFE to prevent all redeclaration errors
- Wrap entire script in IIFE that only runs once
- Check if script already loaded before declaring variables/functions
- Expose initializeFontsTab to window for re-initialization
- Prevents 'Identifier has already been declared' errors on HTMX reload
* fix(web): Exempt config save API endpoints from CSRF protection
- Exempt save_raw_main_config, save_raw_secrets_config, and save_main_config from CSRF
- These endpoints are called via fetch from JavaScript and don't include CSRF tokens
- Fixes 500 error when saving config via raw JSON editor
* fix(web): Exempt system action endpoint from CSRF protection
- Exempt execute_system_action from CSRF
- Fixes 500 error when using system action buttons (restart display, restart Pi, etc.)
- These endpoints are called via HTMX and don't include CSRF tokens
* fix(web): Exempt all API v3 endpoints from CSRF protection
- Add before_request handler to exempt all api_v3.* endpoints
- All API endpoints are programmatic (HTMX/fetch) and don't include CSRF tokens
- Prevents future CSRF errors on any API endpoint
- Cleaner than exempting individual endpoints
* refactor(web): Remove CSRF protection for local-only application
- CSRF is designed for internet-facing apps to prevent cross-site attacks
- For local-only Raspberry Pi app, threat model is different
- All endpoints were exempted anyway, so it wasn't protecting anything
- Forms use HTMX without CSRF tokens
- If exposing to internet later, can re-enable with proper token implementation
* fix(web): Fix font path double-prefixing in font catalog display
- Only prefix with 'assets/fonts/' if path is a bare filename
- If path starts with '/' (absolute) or 'assets/' (already prefixed), use as-is
- Fixes double-prefixing when get_fonts_catalog returns relative paths like 'assets/fonts/press_start.ttf'
* fix(web): Remove fontsTabInitialized guard to allow re-initialization on HTMX reload
- Remove fontsTabInitialized check that prevented re-initialization on HTMX content swap
- The window._fontsScriptLoaded guard is sufficient to prevent function redeclaration
- Allow initializeFontsTab() to run on each HTMX swap to attach listeners to new DOM elements
- Fixes fonts UI breaking after HTMX reload (buttons, upload dropzone, etc. not working)
* fix(api): Preserve empty strings for optional string fields in plugin config
- Add _is_field_required() helper to check if fields are required in schema
- Update _parse_form_value_with_schema() to preserve empty strings for optional string fields
- Fixes 400 error when saving MQTT plugin config with empty username/password
- Resolves validation error: 'Expected type string, got NoneType'
* fix(config): Add defaults to schemas and fix None value handling
- Updated merge_with_defaults to replace None values with defaults
- Fixed form processing to skip empty optional fields without defaults
- Added script to automatically add defaults to all plugin config schemas
- Added defaults to 89 fields across 10 plugin schemas
- Prevents validation errors from None values in configs
Changes:
- schema_manager.py: Enhanced merge_with_defaults to replace None with defaults
- api_v3.py: Added _SKIP_FIELD sentinel to skip optional fields without defaults
- add_defaults_to_schemas.py: Script to add sensible defaults to schemas
- Plugin schemas: Added defaults for number, boolean, and array fields
* fix(config): Fix save button spinner by checking HTTP status code
- Fixed handleConfigSave to check xhr.status instead of event.detail.successful
- With hx-swap="none", HTMX doesn't set event.detail.successful
- Now properly detects successful saves (status 200-299) and stops spinner
- Improved error message extraction from API responses
- Also fixed handleToggleResponse for consistency
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(web): Resolve font display and config API error handling issues
- Fix font catalog display error where path.startsWith fails
(path is object, not string)
- Update save_main_config to use error_response() helper
- Improve save_raw_main_config error handling consistency
- Add proper error codes and traceback details to API responses
* fix(web): Prevent fontCatalog redeclaration error on HTMX reload
- Use window object to store global font variables
- Check if script has already loaded before declaring variables
- Update both window properties and local references on assignment
- Fixes 'Identifier fontCatalog has already been declared' error
* fix(web): Wrap fonts script in IIFE to prevent all redeclaration errors
- Wrap entire script in IIFE that only runs once
- Check if script already loaded before declaring variables/functions
- Expose initializeFontsTab to window for re-initialization
- Prevents 'Identifier has already been declared' errors on HTMX reload
* fix(web): Exempt config save API endpoints from CSRF protection
- Exempt save_raw_main_config, save_raw_secrets_config, and save_main_config from CSRF
- These endpoints are called via fetch from JavaScript and don't include CSRF tokens
- Fixes 500 error when saving config via raw JSON editor
* fix(web): Exempt system action endpoint from CSRF protection
- Exempt execute_system_action from CSRF
- Fixes 500 error when using system action buttons (restart display, restart Pi, etc.)
- These endpoints are called via HTMX and don't include CSRF tokens
* fix(web): Exempt all API v3 endpoints from CSRF protection
- Add before_request handler to exempt all api_v3.* endpoints
- All API endpoints are programmatic (HTMX/fetch) and don't include CSRF tokens
- Prevents future CSRF errors on any API endpoint
- Cleaner than exempting individual endpoints
* refactor(web): Remove CSRF protection for local-only application
- CSRF is designed for internet-facing apps to prevent cross-site attacks
- For local-only Raspberry Pi app, threat model is different
- All endpoints were exempted anyway, so it wasn't protecting anything
- Forms use HTMX without CSRF tokens
- If exposing to internet later, can re-enable with proper token implementation
* fix(web): Fix font path double-prefixing in font catalog display
- Only prefix with 'assets/fonts/' if path is a bare filename
- If path starts with '/' (absolute) or 'assets/' (already prefixed), use as-is
- Fixes double-prefixing when get_fonts_catalog returns relative paths like 'assets/fonts/press_start.ttf'
* fix(web): Remove fontsTabInitialized guard to allow re-initialization on HTMX reload
- Remove fontsTabInitialized check that prevented re-initialization on HTMX content swap
- The window._fontsScriptLoaded guard is sufficient to prevent function redeclaration
- Allow initializeFontsTab() to run on each HTMX swap to attach listeners to new DOM elements
- Fixes fonts UI breaking after HTMX reload (buttons, upload dropzone, etc. not working)
---------
Co-authored-by: Chuck <chuck@example.com>
- Refactor error handling to use async/await for clearer flow
- Store response status before JSON parsing to preserve context
- Extract specific error messages from API responses (data.message)
- Only use generic HTTP status messages when JSON parsing fails
- Ensure API error messages like validation failures are shown to users
instead of generic 'HTTP 500: Internal Server Error'
Fixes issue where specific API error messages were being replaced
with generic HTTP status messages in the catch handler.
Co-authored-by: Chuck <chuck@example.com>
* fix(docs): Add trailing newlines to documentation files
* fix(web): Resolve font configuration loading error on first page load
- Remove ineffective DOMContentLoaded listener from fonts partial (loads via HTMX after main page DOMContentLoaded)
- Add proper HTMX event handling with htmx:afterSettle for reliable initialization
- Add duplicate initialization protection flag
- Improve error handling with response validation and clearer error messages
- Add fallback initialization check for edge cases
- Ensure DOM elements exist before attempting initialization
Fixes issue where 'Error loading font configuration' appeared on first web UI load when opening fonts tab.
* fix(config): Update plugins_directory to plugin-repos in config template
The web-ui-info plugin is located in plugin-repos/ directory, but the
config template was pointing to plugins/ directory. This caused the
plugin to not be discovered on fresh installations.
- Changed plugins_directory from 'plugins' to 'plugin-repos' in config.template.json
- Matches actual plugin location and code default behavior
- Ensures web-ui-info plugin is available by default on fresh installs
* fix(config): Improve config save error handling
- Make load_config() failure non-fatal in save_raw_file_content
- Wrapped reload in try-except to prevent save failures when reload fails
- File save is atomic and successful even if reload fails
- Logs warning when reload fails but doesn't fail the operation
- Improve error messages in API endpoints
- Added detailed error logging with full traceback for debugging
- Extract specific error messages from ConfigError exceptions
- Include config_path in error messages when available
- Provide fallback messages for empty error strings
- Enhance frontend error handling
- Check response status before parsing JSON
- Better handling of non-JSON error responses
- Fallback error messages if error details are missing
Fixes issue where 'Error saving config.json: an error occured' was
shown even when the file was saved successfully but reload failed.
---------
Co-authored-by: Chuck <chuck@example.com>
* fix(docs): Add trailing newlines to documentation files
* fix(web): Resolve font configuration loading error on first page load
- Remove ineffective DOMContentLoaded listener from fonts partial (loads via HTMX after main page DOMContentLoaded)
- Add proper HTMX event handling with htmx:afterSettle for reliable initialization
- Add duplicate initialization protection flag
- Improve error handling with response validation and clearer error messages
- Add fallback initialization check for edge cases
- Ensure DOM elements exist before attempting initialization
Fixes issue where 'Error loading font configuration' appeared on first web UI load when opening fonts tab.
---------
Co-authored-by: Chuck <chuck@example.com>
Chaotic mega-merge into main. THINGS WILL PROBABLY BE BROKEN
* chore: Update soccer-scoreboard submodule to merged commit
- Update submodule reference to include manifest.json v2 registry format
- Version updated to 1.0.1
* refactor: Remove test_mode and logo_dir config reading from base SportsCore
- Remove test_mode initialization and usage
- Remove logo_dir reading from mode_config
- Use LogoDownloader defaults directly for logo directories
* chore: Update plugin submodules after removing global properties
- Update basketball-scoreboard submodule (removed global test_mode, live_priority, dynamic_duration, logo_dir)
- Update soccer-scoreboard submodule (removed global test_mode, live_priority, dynamic_duration, logo_dir)
* feat(calendar): Add credentials.json file upload via web interface
- Add API endpoint /api/v3/plugins/calendar/upload-credentials for file upload
- Validate JSON format and Google OAuth structure
- Save file to plugin directory with secure permissions (0o600)
- Backup existing credentials.json before overwriting
- Add file upload widget support for string fields in config forms
- Add frontend handler handleCredentialsUpload() for single file uploads
- Update .gitignore to allow calendar submodule
- Update calendar submodule reference
* fix(web): Improve spacing for nested configuration sections
- Add dynamic margin based on nesting depth (mb-6 for deeply nested sections)
- Increase padding in nested content areas (py-3 to py-4)
- Add extra spacing after nested sections to prevent overlap
- Enhance CSS spacing for nested sections (1.5rem for nested, 2rem for deeply nested)
- Add padding-bottom to expanded nested content to prevent cutoff
- Fixes issue where game_limits and other nested settings were hidden under next section header
* chore(plugins): Update sports scoreboard plugins with live update interval fix
- Updated hockey-scoreboard, football-scoreboard, basketball-scoreboard, and soccer-scoreboard submodules
- All plugins now fix the interval selection bug that caused live games to update every 5 minutes instead of 30 seconds
- Ensures all live games update at the configured live_update_interval (30s) for timely score updates
* fix: Initialize test_mode in SportsLive and fix config migration
- Add test_mode initialization in SportsLive.__init__() to prevent AttributeError
- Remove invalid new_secrets parameter from save_config_atomic() call in config migration
- Fixes errors: 'NBALiveManager' object has no attribute 'test_mode'
- Fixes errors: ConfigManager.save_config_atomic() got unexpected keyword argument 'new_secrets'
* chore: Update submodules with test_mode initialization fixes
- Update basketball-scoreboard submodule
- Update soccer-scoreboard submodule
* fix(plugins): Auto-stash local changes before plugin updates
- Automatically stash uncommitted changes before git pull during plugin updates
- Prevents update failures when plugins have local modifications
- Improves error messages for git update failures
- Matches behavior of main LEDMatrix update process
* fix(basketball-scoreboard): Update submodule with timeout fix
- Updated basketball-scoreboard plugin to fix update() timeout issue
- Plugin now uses fire-and-forget odds fetching for upcoming games
- Prevents 30-second timeout when processing many upcoming games
Also fixed permission issue on devpi:
- Changed /var/cache/ledmatrix/display_on_demand_state.json permissions
from 600 to 660 to allow web service (devpi user) to read the file
* fix(cache): Ensure cache files use 660 permissions for group access
- Updated setup_cache.sh to set file permissions to 660 (not 775)
- Updated first_time_install.sh to properly set cache file permissions
- Modified DiskCache to set 660 permissions when creating cache files
- Ensures display_on_demand_state.json and other cache files are readable
by web service (devpi user) which is in ledmatrix group
This fixes permission issues where cache files were created with 600
permissions, preventing the web service from reading them. Now files
are created with 660 (rw-rw----) allowing group read access.
* fix(soccer-scoreboard): Update submodule with manifest fix
- Updated soccer-scoreboard plugin submodule
- Added missing entry_point and class_name to manifest.json
- Fixes plugin loading error: 'No class_name in manifest'
Also fixed cache file permissions on devpi server:
- Changed display_on_demand_state.json from 600 to 660 permissions
- Allows web service (devpi user) to read cache files
* fix(display): Remove update_display() calls from clear() to prevent black flash
Previously, display_manager.clear() was calling update_display() twice,
which immediately showed a black screen on the hardware before new
content could be drawn. This caused visible black flashes when switching
between modes, especially when plugins switch from general modes (e.g.,
football_upcoming) to specific sub-modes (e.g., nfl_upcoming).
Now clear() only prepares the buffer without updating the hardware.
Callers can decide when to update the display, allowing smooth transitions
from clear → draw → update_display() without intermediate black flashes.
Places that intentionally show a cleared screen (error cases) already
explicitly call update_display() after clear(), so backward compatibility
is maintained.
* fix(scroll): Prevent wrap-around before cycle completion in dynamic duration
- Check scroll completion BEFORE allowing wrap-around
- Clamp scroll_position when complete to prevent visual loop
- Only wrap-around if cycle is not complete yet
- Fixes issue where stocks plugin showed first stock again at end
- Completion logged only once to avoid spam
- Ensures smooth transition to next mode without visual repeat
* fix(on-demand): Ensure on-demand buttons work and display service runs correctly
- Add early stub functions for on-demand modal to ensure availability when Alpine.js initializes
- Increase on-demand request cache max_age from 5min to 1hr to prevent premature expiration
- Fixes issue where on-demand buttons were not functional due to timing issues
- Ensures display service properly picks up on-demand requests when started
* test: Add comprehensive test coverage (30%+)
- Add 100+ new tests across core components
- Add tests for LayoutManager (27 tests)
- Add tests for PluginLoader (14 tests)
- Add tests for SchemaManager (20 tests)
- Add tests for MemoryCache and DiskCache (24 tests)
- Add tests for TextHelper (9 tests)
- Expand error handling tests (7 new tests)
- Improve coverage from 25.63% to 30.26%
- All 237 tests passing
Test files added:
- test/test_layout_manager.py
- test/test_plugin_loader.py
- test/test_schema_manager.py
- test/test_text_helper.py
- test/test_config_service.py
- test/test_display_controller.py
- test/test_display_manager.py
- test/test_error_handling.py
- test/test_font_manager.py
- test/test_plugin_system.py
Updated:
- pytest.ini: Enable coverage reporting with 30% threshold
- test/conftest.py: Enhanced fixtures for better test isolation
- test/test_cache_manager.py: Expanded cache component tests
- test/test_config_manager.py: Additional config tests
Documentation:
- HOW_TO_RUN_TESTS.md: Guide for running and understanding tests
* test(web): Add comprehensive API endpoint tests
- Add 30 new tests for Flask API endpoints in test/test_web_api.py
- Cover config, system, display, plugins, fonts, and error handling APIs
- Increase test coverage from 30.26% to 30.87%
- All 267 tests passing
Tests cover:
- Config API: GET/POST main config, schedule, secrets
- System API: Status, version, system actions
- Display API: Current display, on-demand start/stop
- Plugins API: Installed plugins, health, config, operations, state
- Fonts API: Catalog, tokens, overrides
- Error handling: Invalid JSON, missing fields, 404s
* test(plugins): Add comprehensive integration tests for all plugins
- Add base test class for plugin integration tests
- Create integration tests for all 6 plugins:
- basketball-scoreboard (11 tests)
- calendar (10 tests)
- clock-simple (11 tests)
- odds-ticker (9 tests)
- soccer-scoreboard (11 tests)
- text-display (12 tests)
- Total: 64 new plugin integration tests
- Increase test coverage from 30.87% to 33.38%
- All 331 tests passing
Tests verify:
- Plugin loading and instantiation
- Required methods (update, display)
- Manifest validation
- Display modes
- Config schema validation
- Graceful handling of missing API credentials
Uses hybrid approach: integration tests in main repo,
plugin-specific unit tests remain in plugin submodules.
* Add mqtt-notifications plugin as submodule
* fix(sports): Respect games_to_show settings for favorite teams
- Fix upcoming games to show N games per team (not just 1)
- Fix recent games to show N games per team (not just 1)
- Add duplicate removal for games involving multiple favorite teams
- Match behavior of basketball-scoreboard plugin
- Affects NFL, NHL, and other sports using base_classes/sports.py
* chore: Remove debug instrumentation logs
- Remove temporary debug logging added during fix verification
- Fix confirmed working by user
* debug: Add instrumentation to debug configuration header visibility issue
* fix: Resolve nested section content sliding under next header
- Remove overflow-hidden from nested-section to allow proper document flow
- Add proper z-index and positioning to prevent overlap
- Add margin-top to nested sections for better spacing
- Remove debug instrumentation that was causing ERR_BLOCKED_BY_CLIENT errors
* fix: Prevent unnecessary plugin tab redraws
- Add check to only update tabs when plugin list actually changes
- Increase debounce timeout to batch rapid changes
- Compare plugin IDs before updating to avoid redundant redraws
- Fix setter to check for actual changes before triggering updates
* fix: Prevent form-groups from sliding out of view when nested sections expand
- Increase margin-bottom on nested-sections for better spacing
- Add clear: both to nested-sections to ensure proper document flow
- Change overflow to visible when expanded to allow natural flow
- Add margin-bottom to expanded content
- Add spacing rules for form-groups that follow nested sections
- Add clear spacer div after nested sections
* fix: Reduce excessive debug logging in generateConfigForm
- Only log once per plugin instead of on every function call
- Prevents log spam when Alpine.js re-renders the form multiple times
- Reduces console noise from 10+ logs per plugin to 1 log per plugin
* fix: Prevent nested section content from sliding out of view when expanded
- Remove overflow-hidden from nested-section in base.html (was causing clipping)
- Add scrollIntoView to scroll expanded sections into view within modal
- Set nested-section overflow to visible to prevent content clipping
- Add min-height to nested-content to ensure proper rendering
- Wait for animation to complete before scrolling into view
* fix: Prevent form-groups from overlapping and appearing outside view
- Change nested-section overflow to hidden by default, visible when expanded
- Add :has() selector to allow overflow when content is expanded
- Ensure form-groups after nested sections have proper spacing and positioning
- Add clear: both and width: 100% to prevent overlap
- Use !important for margin-top to ensure spacing is applied
- Ensure form-groups are in normal document flow with float: none
* fix: Use JavaScript to toggle overflow instead of :has() selector
- :has() selector may not be supported in all browsers
- Use JavaScript to set overflow: visible when expanded, hidden when collapsed
- This ensures better browser compatibility while maintaining functionality
* fix: Make parent sections expand when nested sections expand
- Add updateParentNestedContentHeight() helper to recursively update parent heights
- When a nested section expands, recalculate all parent nested-content max-heights
- Ensures parent sections (like NFL) expand to accommodate expanded child sections
- Updates parent heights both on expand and collapse for proper animation
* refactor: Simplify parent section expansion using CSS max-height: none
- Remove complex recursive parent height update function
- Use CSS max-height: none when expanded to allow natural expansion
- Parent sections automatically expand because nested-content has no height constraint
- Simpler and more maintainable solution
* refactor: Remove complex recursive parent height update function
- CSS max-height: none already handles parent expansion automatically
- No need for JavaScript to manually update parent heights
- Much simpler and cleaner solution
* debug: Add instrumentation to debug auto-collapse issue
- Add logging to track toggle calls and state changes
- Add guard to prevent multiple simultaneous toggles
- Pass event object to prevent bubbling
- Improve state detection logic
- Add return false to onclick handlers
* chore: Remove debug instrumentation from toggleNestedSection
- Remove all debug logging code
- Keep functional fixes: event handling, toggle guard, improved state detection
- Code is now clean and production-ready
* fix(web): Add browser refresh note to plugin fetch errors
* refactor(text-display): Update submodule to use ScrollHelper
* fix(text-display): Fix scrolling display issue - update position in display()
* feat(text-display): Add scroll_loop option and improve scroll speed control
* debug: Add instrumentation to track plugin enabled state changes
Added debug logging to investigate why plugins appear to disable themselves:
- Track enabled state during plugin load (before/after schema merge)
- Track enabled state during plugin reload
- Track enabled state preservation during config save
- Track state reconciliation fixes
- Track enabled state updates in on_config_change
This will help identify which code path is causing plugins to disable.
* debug: Fix debug log path to work on Pi
Changed hardcoded log path to use dynamic project root detection:
- Uses LEDMATRIX_ROOT env var if set
- Falls back to detecting project root by looking for config directory
- Creates .cursor directory if it doesn't exist
- Falls back to /tmp/ledmatrix_debug.log if all else fails
- Added better error handling with logger fallback
* Remove debug instrumentation for plugin enabled state tracking
Removed all debug logging that was added to track plugin enabled state changes.
The instrumentation has been removed as requested.
* Reorganize documentation and cleanup test files
- Move documentation files to docs/ directory
- Remove obsolete test files
- Update .gitignore and README
* feat(text-display): Switch to frame-based scrolling with high FPS support
* fix(text-display): Add backward compatibility for ScrollHelper sub-pixel scrolling
* feat(scroll_helper): Add sub-pixel scrolling support for smooth movement
- Add sub-pixel interpolation using scipy (if available) or numpy fallback
- Add set_sub_pixel_scrolling() method to enable/disable feature
- Implement _get_visible_portion_subpixel() for fractional pixel positioning
- Implement _interpolate_subpixel() for linear interpolation
- Prevents pixel skipping at slow scroll speeds
- Maintains backward compatibility with integer pixel path
* fix(scroll_helper): Reset last_update_time in reset_scroll() to prevent jump-ahead
- Reset last_update_time when resetting scroll position
- Prevents large delta_time on next update after reset
- Fixes issue where scroll would immediately complete again after reset
- Ensures smooth scrolling continuation after loop reset
* fix(scroll_helper): Fix numpy broadcasting error in sub-pixel interpolation
- Add output_width parameter to _interpolate_subpixel() for variable widths
- Fix wrap-around case to use correct widths for interpolation
- Handle edge cases where source array is smaller than expected
- Prevent 'could not broadcast input array' errors in sub-pixel scrolling
- Ensure proper width matching in all interpolation paths
* feat(scroll): Add frame-based scrolling mode for smooth LED matrix movement
- Add frame_based_scrolling flag to ScrollHelper
- When enabled, moves fixed pixels per step, throttled by scroll_delay
- Eliminates time-based jitter by ignoring frame timing variations
- Provides stock-ticker-like smooth, predictable scrolling
- Update text-display plugin to use frame-based mode
This addresses stuttering issues where time-based scrolling caused
visual jitter due to frame timing variations in the main display loop.
* fix(scroll): Fix NumPy broadcasting errors in sub-pixel wrap-around
- Ensure _interpolate_subpixel always returns exactly requested width
- Handle cases where scipy.ndimage.shift produces smaller arrays
- Add padding logic for wrap-around cases when arrays are smaller than expected
- Prevents 'could not broadcast input array' errors during scrolling
* refactor(scroll): Remove sub-pixel interpolation, use high FPS integer scrolling
- Disable sub-pixel scrolling by default in ScrollHelper
- Simplify get_visible_portion to always use integer pixel positioning
- Restore frame-based scrolling logic for smooth high FPS movement
- Use high frame rate (like stock ticker) for smoothness instead of interpolation
- Reduces complexity and eliminates broadcasting errors
* fix(scroll): Prevent large pixel jumps in frame-based scrolling
- Initialize last_step_time properly to prevent huge initial jumps
- Clamp scroll_speed to max 5 pixels/frame in frame-based mode
- Prevents 60-pixel jumps when scroll_speed is misconfigured
- Simplified step calculation to avoid lag catch-up jumps
* fix(text-display): Align config schema and add validation
- Update submodule reference
- Adds warning and logging for scroll_speed config issues
* fix(scroll): Simplify frame-based scrolling to match stock ticker behavior
- Remove throttling logic from frame-based scrolling
- Move pixels every call (DisplayController's loop timing controls rate)
- Add enable_scrolling attribute to text-display plugin for high-FPS treatment
- Matches stock ticker: simple, predictable movement every frame
- Eliminates jitter from timing mismatches between DisplayController and ScrollHelper
* fix(scroll): Restore scroll_delay throttling in frame-based mode
- Restore time-based throttling using scroll_delay
- Move pixels only when scroll_delay has passed
- Handle lag catch-up with reasonable caps to prevent huge jumps
- Preserve fractional timing for smooth operation
- Now scroll_delay actually controls the scroll speed as intended
* feat(text-display): Add FPS counter logging
- Update submodule reference
- Adds FPS tracking and logging every 5 seconds
* fix(text-display): Add display-width buffer so text scrolls completely off
- Update submodule reference
- Adds end buffer to ensure text exits viewport before looping
* fix: Prevent premature game switching in SportsLive
- Set last_game_switch when games load even if current_game already exists
- Set last_game_switch when same games update but it's still 0
- Add guard to prevent switching check when last_game_switch is 0
- Fixes issue where first game shows for only ~2 seconds before switching
- Also fixes random screen flickering when games change prematurely
* feat(plugins): Add branch selection support for plugin installation
- Add optional branch parameter to install_plugin() and install_from_url() in store_manager
- Update API endpoints to accept and pass branch parameter
- Update frontend JavaScript to support branch selection in install calls
- Maintain backward compatibility - branch parameter is optional everywhere
- Falls back to default branch logic if specified branch doesn't exist
* feat(plugins): Add UI for branch selection in plugin installation
- Add branch input field in 'Install Single Plugin' section
- Add global branch input for store installations
- Update JavaScript to read branch from input fields
- Branch input applies to all store installations when specified
* feat(plugins): Change branch selection to be per-plugin instead of global
- Remove global store branch input field
- Add individual branch input field to each plugin card in store
- Add branch input to custom registry plugin cards
- Each plugin can now have its own branch specified independently
* debug: Add logging to _should_exit_dynamic
* feat(display_controller): Add universal get_cycle_duration support for all plugins
UNIVERSAL FEATURE: Any plugin can now implement get_cycle_duration() to dynamically
calculate the total time needed to show all content for a mode.
New method:
- _plugin_cycle_duration(plugin, display_mode): Queries plugin for calculated duration
Integration:
- Display controller calls plugin.get_cycle_duration(display_mode)
- Uses returned duration as target (respecting max cap)
- Falls back to cap if not provided
Benefits:
- Football plugin: Show all games (3 games × 15s = 45s total)
- Basketball plugin: Could implement same logic
- Hockey/Baseball/any sport: Universal support
- Stock ticker: Could calculate based on number of stocks
- Weather: Could calculate based on forecast days
Example plugin implementation:
Result: Plugins control their own display duration based on actual content,
creating a smooth user experience where all content is shown before switching.
* debug: Add logging to cycle duration call
* debug: Change loop exit logs to INFO level
* fix: Change cycle duration logs to INFO level
* fix: Don't exit loop on False for dynamic duration plugins
For plugins with dynamic duration enabled, keep the display loop running
even when display() returns False. This allows games to continue rotating
within the calculated duration.
The loop will only exit when:
- Cycle is complete (plugin reports all content shown)
- Max duration is reached
- Mode is changed externally
* fix(schedule): Improve display scheduling functionality
- Add GET endpoint for schedule configuration retrieval
- Fix mode switching to clean up old config keys (days/start_time/end_time)
- Improve error handling with consistent error_response() usage
- Enhance display controller schedule checking with better edge case handling
- Add validation for time formats and ensure at least one day enabled in per-day mode
- Add debug logging for schedule state changes
Fixes issues where schedule mode switching left stale config causing incorrect behavior.
* fix(install): Add cmake and ninja-build to system dependencies
Resolves h3 package build failure during first-time installation.
The h3 package (dependency of timezonefinder) requires CMake and
Ninja to build from source. Adding these build tools ensures
successful installation of all Python dependencies.
* fix: Pass display_mode in ALL loop calls to maintain sticky manager
CRITICAL FIX: Display controller was only passing display_mode on first call,
causing plugins to fall back to internal mode cycling and bypass sticky
manager logic.
Now consistently passes display_mode=active_mode on every display() call in
both high-FPS and normal loops. This ensures plugins maintain mode context
and sticky manager state throughout the entire display duration.
* feat(install): Add OS check for Raspberry Pi OS Lite (Trixie)
- Verify OS is Raspberry Pi OS (raspbian/debian)
- Require Debian 13 (Trixie) specifically
- Check for Lite version (no desktop environment)
- Exit with clear error message if requirements not met
- Provide instructions for obtaining correct OS version
* fix(web-ui): Add missing notification handlers to quick action buttons
- Added hx-on:htmx:after-request handlers to all quick action buttons in overview.html
- Added hx-ext='json-enc' for proper JSON encoding
- Added missing notification handler for reboot button in index.html
- Users will now see toast notifications when actions complete or fail
* fix(display): Ensure consistent display mode handling in all plugin calls
- Updated display controller to consistently pass display_mode in all plugin display() calls.
- This change maintains the sticky manager state and ensures plugins retain their mode context throughout the display duration.
- Addresses issues with mode cycling and improves overall display reliability.
* fix(display): Enhance display mode persistence across plugin updates
- Updated display controller to ensure display_mode is consistently maintained during plugin updates.
- This change prevents unintended mode resets and improves the reliability of display transitions.
- Addresses issues with mode persistence, ensuring a smoother user experience across all plugins.
* feat: Add Olympics countdown plugin as submodule
- Add olympics-countdown plugin submodule
- Update .gitignore to allow olympics-countdown plugin
- Plugin automatically determines next Olympics and counts down to opening/closing ceremonies
* feat(web-ui): Add checkbox-group widget support for multi-select arrays
- Add checkbox-group widget rendering in plugins_manager.js
- Update form processing to handle checkbox groups with [] naming
- Support for friendly labels via x-options in config schemas
- Update odds-ticker submodule with checkbox-group implementation
* fix(plugins): Preserve enabled state when saving plugin config from main config endpoint
When saving plugin configuration through save_main_config endpoint, the enabled
field was not preserved if missing from the form data. This caused plugins to
be automatically disabled when users saved their configuration from the plugin
manager tab.
This fix adds the same enabled state preservation logic that exists in
save_plugin_config endpoint, ensuring consistent behavior across both endpoints.
The enabled state is preserved from current config, plugin instance, or defaults
to True to prevent unexpected disabling of plugins.
* fix(git): Resolve git status timeout and exclude plugins from base project updates
- Add --untracked-files=no flag to git status for faster execution
- Increase timeout from 5s to 30s for git status operations
- Add timeout exception handling for git status and stash operations
- Filter out plugins directory from git status checks (plugins are separate repos)
- Exclude plugins from stash operations using :!plugins pathspec
- Apply same fixes to plugin store manager update operations
* feat(plugins): Add granular scroll speed control to odds-ticker and leaderboard plugins
- Add display object to both plugins' config schemas with scroll_speed and scroll_delay
- Enable frame-based scrolling mode for precise FPS control (100 FPS for leaderboard)
- Add set_scroll_speed() and set_scroll_delay() methods to both plugins
- Maintain backward compatibility with scroll_pixels_per_second config
- Leaderboard plugin now explicitly sets target_fps to 100 for high-performance scrolling
* fix(scroll): Correct dynamic duration calculation for frame-based scrolling
- Fix calculate_dynamic_duration() to properly handle frame-based scrolling mode
- Convert scroll_speed from pixels/frame to pixels/second when in frame-based mode
- Prevents incorrect duration calculations (e.g., 2609s instead of 52s)
- Affects all plugins using ScrollHelper: odds-ticker, leaderboard, stocks, text-display
- Add debug logging to show scroll mode and effective speed
* Remove version logic from plugin system, use git commits instead
- Remove version parameter from install_plugin() method
- Rename fetch_latest_versions to fetch_commit_info throughout codebase
- Remove version fields from plugins.json registry (versions, latest_version, download_url_template)
- Remove version logging from plugin manager
- Update web UI to use fetch_commit_info parameter
- Update .gitignore to ignore all plugin folders (remove whitelist exceptions)
- Remove plugin directories from git index (plugins now installed via plugin store only)
Plugins now always install latest commit from default branch. Version fields
replaced with git commit SHA and commit dates. System uses git-based approach
for all plugin metadata.
* feat(plugins): Normalize all plugins as git submodules
- Convert all 18 plugins to git submodules for uniform management
- Add submodules for: baseball-scoreboard, christmas-countdown, football-scoreboard, hockey-scoreboard, ledmatrix-flights, ledmatrix-leaderboard, ledmatrix-music, ledmatrix-stocks, ledmatrix-weather, static-image
- Re-initialize mqtt-notifications as proper submodule
- Update .gitignore to allow all plugin submodules
- Add normalize_plugin_submodules.sh script for future plugin management
All plugins with GitHub repositories are now managed as git submodules,
ensuring consistent version control and easier updates.
* refactor(repository): Reorganize scripts and files into organized directory structure
- Move installation scripts to scripts/install/ (except first_time_install.sh)
- Move development scripts to scripts/dev/
- Move utility scripts to scripts/utils/
- Move systemd service files to systemd/
- Keep first_time_install.sh, start_display.sh, stop_display.sh in root
- Update all path references in scripts, documentation, and service files
- Add README.md files to new directories explaining their purpose
- Remove empty tools/ directory (contents moved to scripts/dev/)
- Add .gitkeep to data/ directory
* fix(scripts): Fix PROJECT_DIR path in start_web_conditionally.py after move to scripts/utils/
* fix(scripts): Fix PROJECT_DIR/PROJECT_ROOT path resolution in moved scripts
- Fix wifi_monitor_daemon.py to use project root instead of scripts/utils/
- Fix shell scripts in scripts/ to correctly resolve project root (go up one more level)
- Fix scripts in scripts/fix_perms/ to correctly resolve project root
- Update diagnose_web_interface.sh to reference moved start_web_conditionally.py path
All scripts now correctly determine project root after reorganization.
* fix(install): Update first_time_install.sh to detect and update service files with old paths
- Check for old paths in service files and reinstall if needed
- Always reinstall main service (install_service.sh is idempotent)
- This ensures existing installations get updated paths after reorganization
* fix(install): Update install_service.sh message to indicate it updates existing services
* fix(wifi): Enable WiFi scan to work when AP mode is active
- Temporarily disable AP mode during network scanning
- Automatically re-enable AP mode after scan completes
- Add proper error handling with try/finally to ensure AP mode restoration
- Add user notification when AP mode is temporarily disabled
- Improve error messages for common scanning failures
- Add timing delays for interface mode switching
* fix(wifi): Fix network parsing to handle frequency with 'MHz' suffix
- Strip 'MHz' suffix from frequency field before float conversion
- Add better error logging for parsing failures
- Fixes issue where all networks were silently skipped due to ValueError
* debug(wifi): Add console logging and Alpine.js reactivity fixes for network display
- Add console.log statements to debug network scanning
- Add x-effect to force Alpine.js reactivity updates
- Add unique keys to x-for template
- Add debug display showing network count
- Improve error handling and user feedback
* fix(wifi): Manually update select options instead of using Alpine.js x-for
- Replace Alpine.js x-for template with manual DOM manipulation
- Add updateSelectOptions() method to directly update select dropdown
- This fixes issue where networks weren't appearing in dropdown
- Alpine.js x-for inside select elements can be unreliable
* feat(web-ui): Add patternProperties support for dynamic key-value pairs
- Add UI support for patternProperties objects (custom_feeds, feed_logo_map)
- Implement key-value pair editor with add/remove functionality
- Add JavaScript functions for managing dynamic key-value pairs
- Update form submission to handle patternProperties JSON data
- Enable easy configuration of feed_logo_map in web UI
* chore: Update ledmatrix-news submodule to latest commit
* fix(plugins): Handle arrays of objects in config normalization
Fix configuration validation failure for static-image plugin by adding
recursive normalization support for arrays of objects. The normalize_config_values
function now properly handles arrays containing objects (like image_config.images)
by recursively normalizing each object in the array using the items schema properties.
This resolves the 'configuration validation failed' error when saving static
image plugin configuration with multiple images.
* fix(plugins): Handle union types in config normalization and form generation
Fix configuration validation for fields with union types like ['integer', 'null'].
The normalization function now properly handles:
- Union types in top-level fields (e.g., random_seed: ['integer', 'null'])
- Union types in array items
- Empty string to None conversion for nullable fields
- Form generation and submission for union types
This resolves validation errors when saving plugin configs with nullable
integer/number fields (e.g., rotation_settings.random_seed in static-image plugin).
Also improves UX by:
- Adding placeholder text for nullable fields explaining empty = use default
- Properly handling empty values in form submission for union types
* fix(plugins): Improve union type normalization with better edge case handling
Enhanced normalization for union types like ['integer', 'null']:
- Better handling of whitespace in string values
- More robust empty string to None conversion
- Fallback to None when conversion fails and null is allowed
- Added debug logging for troubleshooting normalization issues
- Improved handling of nested object fields with union types
This should resolve remaining validation errors for nullable integer/number
fields in nested objects (e.g., rotation_settings.random_seed).
* chore: Add ledmatrix-news plugin to .gitignore exceptions
* Fix web interface service script path in install_service.sh
- Updated ExecStart path from start_web_conditionally.py to scripts/utils/start_web_conditionally.py
- Updated diagnose_web_ui.sh to check for correct script path
- Fixes issue where web UI service fails to start due to incorrect script path
* Fix nested configuration section headers not expanding
Fixed toggleNestedSection function to properly calculate scrollHeight when
expanding nested configuration sections. The issue occurred when sections
started with display:none - the scrollHeight was being measured before the
browser had a chance to lay out the element, resulting in a value of 0.
Changes:
- Added setTimeout to delay scrollHeight measurement until after layout
- Added overflow handling during animations to prevent content jumping
- Added fallback for edge cases where scrollHeight might still be 0
- Set maxHeight to 'none' after expansion completes for natural growth
- Updated function in both base.html and plugins_manager.js
This fix applies to all plugins with nested configuration sections, including:
- Hockey/Football/Basketball/Baseball/Soccer scoreboards (customization, global sections)
- All plugins with transition, display, and other nested configuration objects
Fixes configuration header expansion issues across all plugins.
* Fix syntax error in first_time_install.sh step 8.5
Added missing 'fi' statement to close the if block in the WiFi monitor
service installation section. This resolves the 'unexpected end of file'
error that occurred at line 1385 during step 8.5.
* Fix WiFi UI: Display correct SSID and accurate signal strength
- Fix WiFi network selection dropdown not showing available networks
- Replace manual DOM manipulation with Alpine.js x-for directive
- Add fallback watcher to ensure select updates reactively
- Fix WiFi status display showing netplan connection name instead of SSID
- Query actual SSID from device properties (802-11-wireless.ssid)
- Add fallback methods to get SSID from active WiFi connection list
- Improve signal strength accuracy
- Get signal directly from device properties (WIFI.SIGNAL)
- Add multiple fallback methods for robust signal retrieval
- Ensure signal percentage is accurate and up-to-date
* Improve WiFi connection UI and error handling
- Fix connect button disabled condition to check both selectedSSID and manualSSID
- Improve error handling to display actual server error messages from 400 responses
- Add step-by-step labels (Step 1, Step 2, Step 3) to clarify connection workflow
- Add visual feedback showing selected network in blue highlight box
- Improve password field labeling with helpful instructions
- Add auto-clear logic between dropdown and manual SSID entry
- Enhance backend validation with better error messages and logging
- Trim SSID whitespace before processing to prevent validation errors
* Add WiFi disconnect functionality for AP mode testing
- Add disconnect_from_network() method to WiFiManager
- Disconnects from current WiFi network using nmcli
- Automatically triggers AP mode check if auto_enable_ap_mode is enabled
- Returns success/error status with descriptive messages
- Add /api/v3/wifi/disconnect API endpoint
- POST endpoint to disconnect from current WiFi network
- Includes proper error handling and logging
- Add disconnect button to WiFi status section
- Only visible when connected to a network
- Red styling to indicate disconnection action
- Shows 'Disconnecting...' state during operation
- Automatically refreshes status after disconnect
- Integrates with AP mode auto-enable functionality
- When disconnected, automatically enables AP mode if configured
- Perfect for testing captive portal and AP mode features
* Add explicit handling for broken pipe errors during plugin dependency installation
- Catch BrokenPipeError and OSError (errno 32) explicitly in all dependency installation methods
- Add clear error messages explaining network interruption or buffer overflow causes
- Improves error handling in store_manager, plugin_loader, and plugin_manager
- Helps diagnose 'Errno 32 Broken Pipe' errors during pip install operations
* Add WiFi permissions configuration script and integrate into first-time install
- Create configure_wifi_permissions.sh script
- Configures passwordless sudo for nmcli commands
- Configures PolicyKit rules for NetworkManager control
- Fixes 'Not Authorized to control Networking' error
- Allows web interface to connect/disconnect WiFi without password prompts
- Integrate WiFi permissions configuration into first_time_install.sh
- Added as Step 10.1 after passwordless sudo configuration
- Runs automatically during first-time installation
- Ensures WiFi management works out of the box
- Resolves authorization errors when connecting/disconnecting WiFi networks
- NetworkManager requires both sudo and PolicyKit permissions
- Script configures both automatically for seamless WiFi management
* Add WiFi status LED message display integration
- Integrate WiFi status messages from wifi_manager into display_controller
- WiFi status messages interrupt normal rotation (but respect on-demand)
- Priority: on-demand > wifi-status > live-priority > normal rotation
- Safe implementation with comprehensive error handling
- Automatic cleanup of expired/corrupted status files
- Word-wrapping for long messages (max 2 lines)
- Centered text display with small font
- Non-intrusive: all errors are caught and logged, never crash controller
* Fix display loop issues: reduce log spam and handle missing plugins
- Change _should_exit_dynamic logging from INFO to DEBUG to reduce log spam
in tight loops (every 8ms) that was causing high CPU usage
- Fix display loop not running when manager_to_display is None
- Add explicit check to set display_result=False when no plugin manager found
- Fix logic bug where manager_to_display was overwritten after circuit breaker skip
- Ensure proper mode rotation when plugins have no content or aren't found
* Add debug logging to diagnose display loop stuck issue
* Change debug logs to INFO level to diagnose display loop stuck
* Add schedule activation logging and ensure display is blanked when inactive
- Add clear INFO-level log message when schedule makes display inactive
- Track previous display state to detect schedule transitions
- Clear display when schedule makes it inactive to ensure blank screen
(prevents showing initialization screen when schedule kicks in)
- Initialize _was_display_active state tracking in __init__
* Fix indentation errors in schedule state tracking
* Add rotation between hostname and IP address every 10 seconds
- Added _get_local_ip() method to detect device IP address
- Implemented automatic rotation between hostname and IP every 10 seconds
- Enhanced logging to include both hostname and IP in initialization
- Updated get_info() to expose device_ip and current_display_mode
* Add WiFi connection failsafe system
- Save original connection before attempting new connection
- Automatically restore original connection if new connection fails
- Enable AP mode as last resort if restoration fails
- Enhanced connection verification with multiple attempts
- Verify correct SSID (not just 'connected' status)
- Better error handling and exception recovery
- Prevents Pi from becoming unresponsive on connection failure
- Always ensures device remains accessible via original WiFi or AP mode
* feat(web): Improve web UI startup speed and fix cache permissions
- Defer plugin discovery until first API request (removed from startup)
- Add lazy loading to operation queue, state manager, and operation history
- Defer health monitor initialization until first request
- Fix cache directory permission issue:
- Add systemd CacheDirectory feature for automatic cache dir creation
- Add manual cache directory creation in install script as fallback
- Improve cache manager logging (reduce alarming warnings)
- Fix syntax errors in wifi_manager.py (unclosed try blocks)
These changes significantly improve web UI startup time, especially with many
plugins installed, while maintaining full backward compatibility.
* feat(plugins): Improve GitHub token pop-up UX and combine warning/settings
- Fix visibility toggle to handle inline styles properly
- Remove redundant inline styles from HTML elements
- Combine warning banner and settings panel into unified component
- Add loading states to save/load token buttons
- Improve error handling with better user feedback
- Add token format validation (ghp_ or github_pat_ prefix)
- Auto-refresh GitHub auth status after saving token
- Hide warning banner when settings panel opens
- Clear input field after successful save for security
This creates a smoother UX flow where clicking 'Configure Token'
transitions from warning directly to configuration form.
* fix(wifi): Prevent WiFi radio disabling during AP mode disable
- Make NetworkManager restart conditional (only for hostapd mode)
- Add enhanced WiFi radio enable with retry and verification logic
- Add connectivity safety check before NetworkManager restart
- Ensure WiFi radio enabled after all AP mode disable operations
- Fix indentation bug in dnsmasq backup restoration logic
- Add pre-connection WiFi radio check for safety
Fixes issue where WiFi radio was being disabled when disabling AP mode,
especially when connected via Ethernet, making it impossible to enable
WiFi from the web UI.
* fix(plugin-templates): Fix unreachable fallback to expired cache in update() method
The exception handler in update() checked the cached variable, which would
always be None or falsy at that point. If fresh cached data existed, the
method returned early. If cached data was expired, it was filtered out by
max_age constraint. The fix retrieves cached data again in the exception
handler with a very large max_age (1 year) to effectively bypass expiration
check and allow fallback to expired data when fetch fails.
* fix(plugin-templates): Resolve plugin_id mismatch in test template setUp method
* feat(plugins): Standardize manifest version fields schema
- Consolidate version fields to use consistent naming:
- compatible_versions: array of semver ranges (required)
- min_ledmatrix_version: string (optional)
- max_ledmatrix_version: string (optional)
- versions[].ledmatrix_min_version: renamed from ledmatrix_min
- Add manifest schema validation (schema/manifest_schema.json)
- Update store_manager to validate version fields and schema
- Update template and all documentation examples to use standardized fields
- Add deprecation warnings for ledmatrix_version and ledmatrix_min fields
* fix(templates): Update plugin README template script path to correct location
* docs(plugin): Resolve conflicting version management guidance in .cursorrules
* chore(.gitignore): Consolidate plugin exclusion patterns
Remove unnecessary !plugins/*/.git pattern and consolidate duplicate
negations by keeping only trailing-slash directory exclusions.
* docs: Add language specifiers to code blocks in STATIC_IMAGE_MULTI_UPLOAD_PLAN.md
* fix(templates): Remove api_key from config.json example in plugin README template
Remove api_key field from config.json example to prevent credential leakage.
API keys should only be stored in config_secrets.json. Added clarifying note
about proper credential storage.
* docs(README): Add plugin installation and migration information
- Add plugin installation instructions via web interface and GitHub URL
- Add plugin migration guide for users upgrading from old managers
- Improve plugin documentation for new users
* docs(readme): Update donation links and add Discord acknowledgment
* docs: Add comprehensive API references and consolidate documentation
- Add API_REFERENCE.md with complete REST API documentation (50+ endpoints)
- Add PLUGIN_API_REFERENCE.md documenting Display Manager, Cache Manager, and Plugin Manager APIs
- Add ADVANCED_PLUGIN_DEVELOPMENT.md with advanced patterns and examples
- Add DEVELOPER_QUICK_REFERENCE.md for quick developer reference
- Consolidate plugin configuration docs into single PLUGIN_CONFIGURATION_GUIDE.md
- Archive completed implementation summaries to docs/archive/
- Enhance PLUGIN_DEVELOPMENT_GUIDE.md with API links and 3rd party submission guidelines
- Update docs/README.md with new API reference sections
- Update root README.md with documentation links
* fix(install): Fix IP detection and network diagnostics after fresh install
- Fix web-ui-info plugin IP detection to handle no internet, AP mode, and network state changes
- Replace socket-based detection with robust interface scanning using hostname -I and ip addr
- Add AP mode detection returning 192.168.4.1 when AP mode is active
- Add periodic IP refresh every 30 seconds to handle network state changes
- Improve network diagnostics in first_time_install.sh showing actual IPs, WiFi status, and AP mode
- Add WiFi connection check in WiFi monitor installation with warnings
- Enhance web service startup logging to show accessible IP addresses
- Update README with network troubleshooting section and fix port references (5001->5000)
Fixes issue where display showed incorrect IP (127.0.11:5000) and users couldn't access web UI after fresh install.
* chore: Add GitHub sponsor button configuration
* fix(wifi): Fix aggressive AP mode enabling and improve WiFi detection
Critical fixes:
- Change auto_enable_ap_mode default from True to False (manual enable only)
- Fixes issue where Pi would disconnect from network after code updates
- Matches documented behavior (was incorrectly defaulting to True in code)
Improvements:
- Add grace period: require 3 consecutive disconnected checks (90s) before enabling AP mode
- Prevents AP mode from enabling on transient network hiccups
- Improve WiFi status detection with retry logic and better nmcli parsing
- Enhanced logging for debugging WiFi connection issues
- Better handling of WiFi device detection (works with any wlan device)
This prevents the WiFi monitor from aggressively enabling AP mode and
disconnecting the Pi from the network when there are brief network issues
or during system initialization.
* fix(wifi): Revert auto_enable_ap_mode default to True with grace period protection
Change default back to True for auto_enable_ap_mode while keeping the grace
period protection that prevents interrupting valid WiFi connections.
- Default auto_enable_ap_mode back to True (useful for setup scenarios)
- Grace period (3 consecutive checks = 90s) prevents false positives
- Improved WiFi detection with retry logic ensures accurate status
- AP mode will auto-enable when truly disconnected, but won't interrupt
valid connections due to transient detection issues
* fix(news): Update submodule reference for manifest fix
Update ledmatrix-news submodule to include the fixed manifest.json with
required entry_point and class_name fields.
* fix(news): Update submodule reference with validate_config addition
Update ledmatrix-news submodule to include validate_config method for
proper configuration validation.
* feat: Add of-the-day plugin as git submodule
- Add ledmatrix-of-the-day plugin as git submodule
- Rename submodule path from plugins/of-the-day to plugins/ledmatrix-of-the-day to match repository naming convention
- Update .gitignore to allow ledmatrix-of-the-day submodule
- Plugin includes fixes for display rendering and web UI configuration support
* fix(wifi): Make AP mode open network and fix WiFi page loading in AP mode
AP Mode Changes:
- Remove password requirement from AP mode (open network for easier setup)
- Update hostapd config to create open network (no WPA/WPA2)
- Update nmcli hotspot to create open network (no password parameter)
WiFi Page Loading Fixes:
- Download local copies of HTMX and Alpine.js libraries
- Auto-detect AP mode (192.168.4.x) and use local JS files instead of CDN
- Auto-open WiFi tab when accessing via AP mode IP
- Add fallback loading if HTMX fails to load
- Ensures WiFi setup page works in AP mode without internet access
This fixes the issue where the WiFi page wouldn't load on iPhone when
accessing via AP mode (192.168.4.1:5000) because CDN resources couldn't
be fetched without internet connectivity.
* feat(wifi): Add explicit network switching support with clean disconnection
WiFi Manager Improvements:
- Explicitly disconnect from current network before connecting to a new one
- Add skip_ap_check parameter to disconnect_from_network() to prevent AP mode
from activating during network switches
- Check if already connected to target network to avoid unnecessary work
- Improved logging for network switching operations
Web UI Improvements:
- Detect and display network switching status in UI
- Show 'Switching from [old] to [new]...' message when switching networks
- Enhanced status reloading after connection (multiple checks at 2s, 5s, 10s)
- Better user feedback during network transitions
This ensures clean network switching without AP mode interruptions and
provides clear feedback to users when changing WiFi networks.
* fix(web-ui): Add fallback content loading when HTMX fails to load
Problem:
- After recent updates, web UI showed navigation and CPU status but main
content tabs never loaded
- Content tabs depend on HTMX's 'revealed' trigger to load
- If HTMX failed to load or initialize, content would never appear
Solutions:
- Enhanced HTMX loading verification with timeout checks
- Added fallback direct fetch for overview tab if HTMX fails
- Added automatic tab content loading when tabs change
- Added loadTabContent() method to manually trigger content loading
- Added global 'htmx-load-failed' event for error handling
- Automatic retry after 5 seconds if HTMX isn't available
- Better error messages and console logging for debugging
This ensures the web UI loads content even if HTMX has issues,
providing graceful degradation and better user experience.
* feat(web-ui): Add support for plugin custom HTML widgets and static file serving
- Add x-widget: custom-html support in config schema generation
- Add loadCustomHtmlWidget() function to load HTML from plugin directories
- Add /api/v3/plugins/<plugin_id>/static/<file_path> endpoint for serving plugin static files
- Enhance execute_plugin_action() to pass params via stdin as JSON for scripts
- Add JSON output parsing for script action responses
These changes enable plugins to provide custom UI components while keeping
all functionality plugin-scoped. Used by of-the-day plugin for file management.
* fix(web-ui): Resolve Alpine.js initialization errors
- Prevent Alpine.js from auto-initializing before app() function is defined
- Add deferLoadingAlpine to ensure proper initialization order
- Make app() function globally available via window.app
- Fix 'app is not defined' and 'activeTab is not defined' errors
- Remove duplicate Alpine.start() calls that caused double initialization warnings
* fix(web-ui): Fix IndentationError in api_v3.py OAuth flow
- Fix indentation in if action_def.get('oauth_flow') block
- Properly indent try/except block and all nested code
- Resolves IndentationError that prevented web interface from starting
* fix(web-ui): Fix SyntaxError in api_v3.py else block
- Fix indentation of OAuth flow code inside else block
- Properly indent else block for simple script execution
- Resolves SyntaxError at line 3458 that prevented web interface from starting
* fix(web-ui): Restructure OAuth flow check to fix SyntaxError
- Move OAuth flow check before script execution in else block
- Remove unreachable code that was causing syntax error
- OAuth check now happens first, then falls back to script execution
- Resolves SyntaxError at line 3458
* fix(web-ui): Define app() function in head for Alpine.js initialization
- Define minimal app() function in head before Alpine.js loads
- Ensures app() is available when Alpine initializes
- Full implementation in body enhances/replaces the stub
- Fixes 'app is not defined' and 'activeTab is not defined' errors
* fix(web-ui): Ensure plugin tabs load when full app() implementation is available
- Update stub init() to detect and use full implementation when available
- Ensure full implementation properly replaces stub methods
- Call init() after merging to load plugins and set up watchers
- Fixes issue where installed plugins weren't showing in navigation bar
* fix(web-ui): Prevent 'Cannot redefine property' error for installedPlugins
- Check if window.installedPlugins property already exists before defining
- Make property configurable to allow redefinition if needed
- Add _initialized flag to prevent multiple init() calls
- Fixes TypeError when stub tries to enhance with full implementation
* fix(web-ui): Fix variable redeclaration errors in logs tab
- Replace let/const declarations with window properties to avoid redeclaration
- Use window._logsEventSource, window._allLogs, etc. to persist across HTMX reloads
- Clean up existing event source before reinitializing
- Remove and re-add event listeners to prevent duplicates
- Fixes 'Identifier has already been declared' error when accessing logs tab multiple times
* feat(web-ui): Add support for additionalProperties object rendering
- Add handler for objects with additionalProperties containing object schemas
- Render dynamic category controls with enable/disable toggles
- Display category metadata (display name, data file path)
- Used by of-the-day plugin for category management
* fix(wifi): Ensure AP mode hotspot is always open (no password)
Problem:
- LEDMatrix-Setup WiFi AP was still asking for password despite code changes
- Existing hotspot connections with passwords weren't being fully cleaned up
- NetworkManager might reuse old connection profiles with passwords
Solutions:
- More thorough cleanup: Delete all hotspot-related connections, not just known names
- Verification: Check if hotspot has password after creation
- Automatic fix: Remove password and restart connection if security is detected
- Better logging: Log when password is detected and removed
This ensures the AP mode hotspot is always open for easy setup access,
even if there were previously saved connections with passwords.
* fix(wifi): Improve network switching reliability and device state handling
Problem:
- Pi failing to switch WiFi networks via web UI
- Connection attempts happening before device is ready
- Disconnect not fully completing before new connection attempt
- Connection name lookup issues when SSID doesn't match connection name
Solutions:
- Improved disconnect logic: Disconnect specific connection first, then device
- Device state verification: Wait for device to be ready (disconnected/unavailable) before connecting
- Better connection lookup: Search by SSID, not just connection name
- Increased wait times: 2 seconds for disconnect to complete
- State checking before activating existing connections
- Enhanced error handling and logging throughout
This ensures network switching works reliably by properly managing device
state transitions and using correct connection identifiers.
* debug(web-ui): Add debug logging for custom HTML widget loading
- Add console logging to track widget generation
- Improve error messages with missing configuration details
- Help diagnose why file manager widget may not be appearing
* fix(web-ui): Fix [object Object] display in categories field
- Add type checking to ensure category values are strings before rendering
- Safely extract data_file and display_name properties
- Prevent object coercion issues in category display
* perf(web-ui): Optimize plugin loading in navigation bar
- Reduce stub init timeout from 100ms to 10ms for faster enhancement
- Change full implementation merge from 50ms setTimeout to requestAnimationFrame
- Add direct plugin loading in stub while waiting for full implementation
- Skip plugin reload in full implementation if already loaded by stub
- Significantly improves plugin tab loading speed in navigation bar
* feat(web-ui): Adapt file-upload widget for JSON files in of-the-day plugin
- Add specialized JSON upload/delete endpoints for of-the-day plugin
- Modify file-upload widget to support JSON files (file_type: json)
- Render JSON files with file-code icon instead of image preview
- Show entry count for JSON files
- Store files in plugins/ledmatrix-of-the-day/of_the_day/ directory
- Automatically update categories config when files are uploaded/deleted
- Populate uploaded_files array from categories on form load
- Remove custom HTML widget, use standard file-upload widget instead
* fix(web-ui): Add working updatePluginTabs to stub for immediate plugin tab rendering
- Stub's updatePluginTabs was empty, preventing tabs from showing
- Add basic implementation that creates plugin tabs in navigation bar
- Ensures plugin tabs appear immediately when plugins load, even before full implementation merges
- Fixes issue where plugin navigation bar wasn't working
* feat(api): Populate uploaded_files and categories from disk for of-the-day plugin
- Scan of_the_day directory for existing JSON files when loading config
- Populate uploaded_files array from files on disk
- Populate categories from files on disk if not in config
- Categories default to disabled, user can enable them
- Ensures existing JSON files (word_of_the_day.json, slovenian_word_of_the_day.json) appear in UI
* fix(api): Improve category merging logic for of-the-day plugin
- Preserve existing category enabled state when merging with files from disk
- Ensure all JSON files from disk appear in categories section
- Categories from files default to disabled, preserving user choices
- Properly merge existing config with scanned files
* fix(wifi): More aggressive password removal for AP mode hotspot
Problem:
- LEDMatrix-Setup network still asking for password despite previous fixes
- NetworkManager may add default security settings to hotspots
- Existing connections with passwords may not be fully cleaned up
Solutions:
- Always remove ALL security settings after creating hotspot (not just when detected)
- Remove multiple security settings: key-mgmt, psk, wep-key, auth-alg
- Verify security was removed and recreate connection if verification fails
- Improved cleanup: Delete connections by SSID match, not just by name
- Disconnect connections before deleting them
- Always restart connection after removing security to apply changes
- Better logging for debugging
This ensures the AP mode hotspot is always open, even if NetworkManager
tries to add default security settings.
* perf(web): Optimize web interface performance and fix JavaScript errors
- Add resource hints (preconnect, dns-prefetch) for CDN resources to reduce DNS lookup delays
- Fix duplicate response parsing bug in loadPluginConfig that was parsing JSON twice
- Replace direct fetch() calls with PluginAPI.getInstalledPlugins() to leverage caching and throttling
- Fix Alpine.js function availability issues with defensive checks and $nextTick
- Enhance request deduplication with debug logging and statistics
- Add response caching headers for static assets and API responses
- Add performance monitoring utilities with detailed metrics
Fixes console errors for loadPluginConfig and generateConfigForm not being defined.
Reduces duplicate API calls to /api/v3/plugins/installed endpoint.
Improves initial page load time with resource hints and optimized JavaScript loading.
* perf(web-ui): optimize CSS for Raspberry Pi performance
- Remove backdrop-filter blur from modal-backdrop
- Remove box-shadow transitions (use transform/opacity only)
- Remove button ::before pseudo-element animation
- Simplify skeleton loader (gradient to opacity pulse)
- Optimize transition utility (specific properties, not 'all')
- Improve color contrast for WCAG AA compliance
- Add CSS containment to cards, plugin-cards, modals
- Remove unused CSS classes (duration-300, divider, divider-light)
- Remove duplicate spacing utility classes
All animations now GPU-accelerated (transform/opacity only).
Optimized for low-powered Raspberry Pi devices.
* fix(web): Resolve ReferenceError for getInstalledPluginsSafe in v3 stub initialization
Move getInstalledPluginsSafe() function definition before the app() stub code that uses it. The function was previously defined at line 3756 but was being called at line 849 during Alpine.js initialization, causing a ReferenceError when loadInstalledPluginsDirectly() attempted to load plugins before the full implementation was ready.
* fix(web): Resolve TypeError for installedPlugins.map in plugin loading
Fix PluginAPI.getInstalledPlugins() to properly extract plugins array from API response structure. The API returns {status: 'success', data: {plugins: [...]}}, but the method was returning response.data (the object) instead of response.data.plugins (the array).
Changes:
- api_client.js: Extract plugins array from response.data.plugins
- plugins_manager.js: Add defensive array checks and handle array return value correctly
- base.html: Add defensive check in getInstalledPluginsSafe() to ensure plugins is always an array
This prevents 'installedPlugins.map is not a function' errors when loading plugins.
* style(web-ui): Enhance navigation bar styling for better readability
- Improve contrast: Change inactive tab text from gray-500 to gray-700
- Add gradient background and thicker border for active tabs
- Enhance hover states with background highlights
- Add smooth transitions using GPU-accelerated properties
- Update all navigation buttons (system tabs and plugin tabs)
- Add updatePluginTabStates() method for dynamic tab state management
All changes are CSS-only with zero performance overhead.
* fix(web-ui): Optimize plugin loading and reduce initialization errors
- Make generateConfigForm accessible to inline Alpine components via parent scope
- Consolidate plugin initialization to prevent duplicate API calls
- Fix script execution from HTMX-loaded content by extracting scripts before DOM insertion
- Add request deduplication to loadInstalledPlugins() to prevent concurrent requests
- Improve Alpine component initialization with proper guards and fallbacks
This eliminates 'generateConfigForm is not defined' errors and reduces plugin
API calls from 3-4 duplicate calls to 1 per page load, significantly improving
page load performance.
* fix(web-ui): Add guard check for generateConfigForm to prevent Alpine errors
Add typeof check in x-show to prevent Alpine from evaluating generateConfigForm
before the component methods are fully initialized. This eliminates the
'generateConfigForm is not defined' error that was occurring during component
initialization.
* fix(web-ui): Fix try-catch block structure in script execution code
Correct the nesting of try-catch block inside the if statement for script execution.
The catch block was incorrectly placed after the else clause, causing a syntax error.
* fix(web-ui): Escape quotes in querySelector to avoid HTML attribute conflicts
Change double quotes to single quotes in the CSS selector to prevent conflicts
with HTML attribute parsing when the x-data expression is embedded.
* style(web): Improve button text readability in Quick Actions section
* fix(web): Resolve Alpine.js expression errors in plugin configuration component
- Capture plugin from parent scope into component data to fix parsing errors
- Update all plugin references to use this.plugin in component methods
- Fix x-init to properly call loadPluginConfig method
- Resolves 'Uncaught ReferenceError' for isOnDemandLoading, onDemandLastUpdated, and other component properties
* fix(web): Fix remaining Alpine.js scope issues in plugin configuration
- Use this.generateConfigForm in typeof checks and method calls
- Fix form submission to use this.plugin.id
- Use $root. prefix for parent scope function calls (refreshPlugin, updatePlugin, etc.)
- Fix confirm dialog string interpolation
- Ensures all component methods and properties are properly scoped
* fix(web): Add this. prefix to all Alpine.js component property references
- Fix all template expressions to use this. prefix for component properties
- Update isOnDemandLoading, onDemandLastUpdated, onDemandRefreshing references
- Update onDemandStatusClass, onDemandStatusText, onDemandServiceClass, onDemandServiceText
- Update disableRunButton, canStopOnDemand, showEnableHint, loading references
- Ensures Alpine.js can properly resolve all component getters and properties
* fix(web): Resolve Alpine.js expression errors in plugin configuration
- Move complex x-data object to pluginConfigData() function for better parsing
- Fix all template expressions to use this.plugin instead of plugin
- Add this. prefix to all method calls in event handlers
- Fix duplicate x-on:click attribute on uninstall button
- Add proper loading state management in loadPluginConfig method
This resolves the 'Invalid or unexpected token' and 'Uncaught ReferenceError'
errors in the browser console.
* fix(web): Fix plugin undefined errors in Alpine.js plugin configuration
- Change x-data initialization to capture plugin from loop scope first
- Use Object.assign in x-init to merge pluginConfigData properties
- Add safety check in pluginConfigData function for undefined plugins
- Ensure plugin is available before accessing properties in expressions
This resolves the 'Cannot read properties of undefined' errors by ensuring
the plugin object is properly captured from the x-for loop scope before
any template expressions try to access it.
* style(web): Make Quick Actions button text styling consistent
- Update Start Display, Stop Display, and Reboot System buttons
- Change from text-sm font-medium to text-base font-semibold
- All Quick Actions buttons now have consistent bold, larger text
- Matches the styling of Update Code, Restart Display Service, and Restart Web Service buttons
* fix(wifi): Properly handle AP mode disable during WiFi connection
- Check return value of disable_ap_mode() before proceeding with connection
- Add verification loop to ensure AP mode is actually disabled
- Increase wait time to 5 seconds for NetworkManager restart stabilization
- Return clear error messages if AP mode cannot be disabled
- Prevents connection failures when switching networks from web UI or AP mode
This fixes the issue where WiFi network switching would fail silently when
AP mode disable failed, leaving the system in an inconsistent state.
* fix(web): Handle API response errors in plugin configuration loading
- Add null/undefined checks before accessing API response status
- Set fallback defaults when API responses don't have status 'success'
- Add error handling for batch API requests with fallback to individual requests
- Add .catch() handlers to individual fetch calls to prevent unhandled rejections
- Add console warnings to help debug API response failures
- Fix applies to both main loadPluginConfig and PluginConfigHelpers.loadPluginConfig
This fixes the issue where plugin configuration sections would get stuck
showing the loading animation when API responses failed or returned error status.
* fix(web): Fix Alpine.js reactivity for plugin config by using direct x-data
Changed from Object.assign pattern to direct x-data assignment to ensure
Alpine.js properly tracks reactive properties. The previous approach used
Object.assign to merge properties into the component after initialization,
which caused Alpine to not detect changes to config/schema properties.
The fix uses pluginConfigData(plugin) directly as x-data, ensuring all
properties including config, schema, loading, etc. are reactive from
component initialization.
* fix(web): Ensure plugin variable is captured in x-data scope
Use spread operator to merge pluginConfigData properties while explicitly
capturing the plugin variable from outer x-for scope. This fixes undefined
plugin errors when Alpine evaluates the component data.
* fix(web): Use $data for Alpine.js reactivity when merging plugin config
Use Object.assign with Alpine's $data reactive proxy instead of this to
ensure added properties are properly reactive. This fixes the issue where
plugin variable scoping from x-for wasn't accessible in x-data expressions.
* fix(web): Remove incorrect 'this.' prefix in Alpine.js template expressions
Alpine.js template expressions (x-show, x-html, x-text, x-on) use the
component data as the implicit context, so 'this.' prefix is incorrect.
In template expressions, 'this' refers to the DOM element, not the
component data.
Changes:
- Replace 'this.plugin.' with 'plugin.' in all template expressions (19 instances)
- Replace 'this.loading' with 'loading' in x-show directives
- Replace 'this.generateConfigForm' with 'generateConfigForm' in x-show/x-html
- Replace 'this.savePluginConfig' with 'savePluginConfig' in x-on:submit
- Replace 'this.config/schema/webUiActions' with direct property access
- Use '$data.loadPluginConfig' in x-init for explicit method call
Note: 'this.' is still correct inside JavaScript method definitions within
pluginConfigData() function since those run with proper object context.
* fix(web): Prevent infinite recursion in plugin config methods
Add 'parent !== this' check to loadPluginConfig, generateConfigForm, and
savePluginConfig methods in pluginConfigData to prevent infinite recursion
when the component tries to delegate to a parent that resolves to itself.
This fixes the 'Maximum call stack size exceeded' error that occurred when
the nested Alpine component's $root reference resolved to a component that
had the same delegating methods via Object.assign.
* fix(web): Resolve infinite recursion in plugin config by calling $root directly
The previous implementation had delegating methods (generateConfigForm,
savePluginConfig) in pluginConfigData that tried to call parent.method(),
but the parent detection via getParentApp() was causing circular calls
because multiple components had the same methods.
Changes:
- Template now calls $root.generateConfigForm() and $root.savePluginConfig()
directly instead of going through nested component delegation
- Removed delegating generateConfigForm and savePluginConfig from pluginConfigData
- Removed getParentApp() helper that was enabling the circular calls
- Simplified loadPluginConfig to use PluginConfigHelpers directly
This fixes the 'Maximum call stack size exceeded' error when rendering
plugin configuration forms.
* fix(web): Use window.PluginConfigHelpers instead of $root for plugin config
The $root magic variable in Alpine.js doesn't correctly reference the
app() component's data scope from nested x-data contexts. This causes
generateConfigForm and savePluginConfig to be undefined.
Changed to use window.PluginConfigHelpers which has explicit logic to
find and use the app component's methods.
* fix(web): Use direct x-data initialization for plugin config reactivity
Changed from Object.assign($data, pluginConfigData(plugin)) to
x-data="pluginConfigData(plugin)" to ensure Alpine.js properly
tracks reactivity for all plugin config properties. This fixes
the issue where all plugin tabs were showing the same config.
* refactor(web): Implement server-side plugin config rendering with HTMX
Major architectural improvement to plugin configuration management:
- Add server-side Jinja2 template for plugin config forms
(web_interface/templates/v3/partials/plugin_config.html)
- Add Flask route to serve plugin config partials on-demand
- Replace complex client-side form generation with HTMX lazy loading
- Add Alpine.js store for centralized plugin state management
- Mark old pluginConfigData and PluginConfigHelpers as deprecated
Benefits:
- Lazy loading: configs only load when tab is accessed
- Server-side rendering: reduces client-side complexity
- Better performance: especially on Raspberry Pi
- Cleaner code: Jinja2 macros replace JS string templates
- More maintainable: form logic in one place (server)
The old client-side code is preserved for backwards compatibility
but is no longer used by the main plugin configuration UI.
* fix(web): Trigger HTMX manually after Alpine renders plugin tabs
HTMX processes attributes at page load time, before Alpine.js
renders dynamic content. Changed from :hx-get attribute to
x-init with htmx.ajax() to properly trigger the request after
the element is rendered.
* fix(web): Remove duplicate 'enabled' toggle from plugin config form
The 'enabled' field was appearing twice in plugin configuration:
1. Header toggle (quick action, uses HTMX)
2. Configuration form (from schema, requires save)
Now only the header toggle is shown, avoiding user confusion.
The 'enabled' key is explicitly skipped when rendering schema properties.
* perf(web): Optimize plugin manager with request caching and init guards
Major performance improvements to plugins_manager.js:
1. Request Deduplication & Caching
- Added pluginLoadCache with 3-second TTL
- Subsequent calls return cached data instead of making API requests
- In-flight request deduplication prevents parallel duplicate fetches
- Added refreshInstalledPlugins() for explicit force-refresh
2. Initialization Guards
- Added pluginsInitialized flag to prevent multiple initializePlugins() calls
- Added _eventDelegationSetup guard on container to prevent duplicate listeners
- Added _listenerSetup guards on search/category inputs
3. Debug Logging Control
- Added PLUGIN_DEBUG flag (localStorage.setItem('pluginDebug', 'true'))
- Most console.log calls now use pluginLog() which only logs when debug enabled
- Reduces console noise from ~150 logs to ~10 in production
Expected improvements:
- API calls reduced from 6+ to 2 on page load
- Event listeners no longer duplicated
- Cleaner console output
- Faster perceived performance
* fix(web): Handle missing search elements in searchPluginStore
The searchPluginStore function was failing silently when called before
the plugin-search and plugin-category elements existed in the DOM.
This caused the plugin store to never load.
Now safely checks if elements exist before accessing their values.
* fix(web): Ensure plugin store loads via pluginManager.searchPluginStore
- Exposed searchPluginStore on window.pluginManager for easier access
- Updated base.html to fallback to pluginManager.searchPluginStore
- Added logging when loading plugin store
* fix(web): Expose searchPluginStore from inside the IIFE
The function was defined inside the IIFE but only exposed after the IIFE
ended, where the function was out of scope. Now exposed immediately after
definition inside the IIFE.
* fix(web): Add cache-busting version to plugins_manager.js URL
Static JS files were being aggressively cached, preventing updates
from being loaded by browsers.
* fix(web): Fix pluginLog reference error outside IIFE
pluginLog is defined inside the IIFE, so use _PLUGIN_DEBUG_EARLY and
console.log directly for code outside the IIFE.
* chore(web): Update plugins_manager.js cache version
* fix(web): Defer plugin store render when grid not ready
Instead of showing an error when plugin-store-grid doesn't exist,
store plugins in window.__pendingStorePlugins for later rendering
when the tab loads (consistent with how installed plugins work).
* chore: Bump JS cache version
* fix(web): Restore enabledBool variable in plugin render
Variable was removed during debug logging optimization but was still
being used in the template string for toggle switch rendering.
* fix(ui): Add header and improve categories section rendering
- Add proper header (h4) to categories section with label
- Add debug logging to diagnose categories field rendering
- Improve additionalProperties condition check readability
* fix(ui): Improve additionalProperties condition check
- Explicitly exclude objects with properties to avoid conflicts
- Ensure categories section is properly detected and rendered
- Categories should show as header with toggles, not text box
* fix(web-ui): Fix JSON parsing errors and default value loading for plugin configs
- Fix JSON parsing errors when saving file upload fields by properly unescaping HTML entities
- Merge config with schema defaults when loading plugin config so form shows default values
- Improve default value handling in form generation for nested objects and arrays
- Add better error handling for malformed JSON in file upload fields
* fix(plugins): Return plugins array from getInstalledPlugins() instead of data object
Fixed PluginAPI.getInstalledPlugins() to return response.data.plugins (array)
instead of response.data (object). This was preventing window.installedPlugins
from being set correctly, which caused plugin configuration tabs to not appear
and prevented users from saving plugin configurations via the web UI.
The fix ensures that:
- window.installedPlugins is properly populated with plugin array
- Plugin tabs are created automatically on page load
- Configuration forms and save buttons are rendered correctly
- Save functionality works as expected
* fix(api): Support form data submission for plugin config saves
The HTMX form submissions use application/x-www-form-urlencoded format
instead of JSON. This update allows the /api/v3/plugins/config POST
endpoint to accept both formats:
- JSON: plugin_id and config in request body (existing behavior)
- Form data: plugin_id from query string, config fields from form
Added _parse_form_value helper to properly convert form strings to
appropriate Python types (bool, int, float, JSON arrays/objects).
* debug: Add form data logging to diagnose config save issue
* fix(web): Re-discover plugins before loading config partial
The plugin config partial was returning 'not found' for plugins
because the plugin manifests weren't loaded. The installed plugins
API was working because it calls discover_plugins() first.
Changes:
- Add discover_plugins() call in _load_plugin_config_partial when
plugin info is not found on first try
- Remove debug logging from form data handling
* fix(web): Comprehensive plugin config save improvements
SWEEPING FIX for plugin configuration saving issues:
1. Form data now MERGES with existing config instead of replacing
- Partial form submissions (missing fields) no longer wipe out
existing config values
- Fixes plugins with complex schemas (football, clock, etc.)
2. Improved nested value handling with _set_nested_value helper
- Correctly handles deeply nested structures like customization
- Properly merges when intermediate objects already exist
3. Better JSON parsing for arrays
- RGB color arrays like [255, 0, 0] now parse correctly
- Parse JSON before trying number conversion
4. Bump cache version to force JS reload
* fix(web): Add early stubs for updatePlugin and uninstallPlugin
Ensures these functions are available immediately when the page loads,
even before the full IIFE executes. Provides immediate user feedback
and makes API calls directly.
This fixes the 'Update button does not work' issue by ensuring the
function is always defined and callable.
* fix(web): Support form data in toggle endpoint
The toggle endpoint now accepts both JSON and HTMX form submissions.
Also updated the plugin config template to send the enabled state
via hx-vals when the checkbox changes.
Fixes: 415 Unsupported Media Type error when toggling plugins
* fix(web): Prevent config duplication when toggling plugins
Changed handleToggleResponse to update UI in place instead of
refreshing the entire config partial, which was causing duplication.
Also improved refreshPluginConfig with proper container targeting
and concurrent refresh prevention (though it's no longer needed
for toggles since we update in place).
* fix(api): Schema-aware form value parsing for plugin configs
Major fix for plugin config saving issues:
1. Load schema BEFORE processing form data to enable type-aware parsing
2. New _parse_form_value_with_schema() function that:
- Converts comma-separated strings to arrays when schema says 'array'
- Parses JSON strings for arrays/objects
- Handles empty strings for arrays (returns [] instead of None)
- Uses schema to determine correct number types
3. Post-processing to ensure None arrays get converted to empty arrays
4. Proper handling of nested object fields
Fixes validation errors:
- 'category_order': Expected type array, got str
- 'categories': Expected type object, got str
- 'uploaded_files': Expected type array, got NoneType
- RGB color arrays: Expected type array, got str
* fix(web): Make plugin config handlers idempotent and remove scripts from HTMX partials
CRITICAL FIX for script redeclaration errors:
1. Removed all <script> tags from plugin_config.html partial
- Scripts were being re-executed on every HTMX swap
- Caused 'Identifier already declared' errors
2. Moved all handler functions to base.html with idempotent initialization
- Added window.__pluginConfigHandlersInitialized guard
- Functions only initialized once, even if script runs multiple times
- All state stored on window object (e.g., window.pluginConfigRefreshInProgress)
3. Enhanced error logging:
- Client-side: Logs form payload, response status, and parsed error details
- Server-side: Logs raw form data and parsed config on validation failures
4. Functions moved to window scope:
- toggleSection
- handleConfigSave (with detailed error logging)
- handleToggleResponse (updates UI in place, no refresh)
- handlePluginUpdate
- refreshPluginConfig (with duplicate prevention)
- runPluginOnDemand
- stopOnDemand
- executePluginAction
This ensures HTMX-swapped fragments only contain HTML, and all
scripts run once in the base layout.
* fix(api): Filter config to only schema-defined fields before validation
When merging with existing_config, fields not in the plugin's schema
(like high_performance_transitions, transition, dynamic_duration)
were being preserved, causing validation failures when
additionalProperties is false.
Add _filter_config_by_schema() function to recursively filter config
to only include fields defined in the schema before validation.
This fixes validation errors like:
- 'Additional properties are not allowed (high_performance_transitions, transition were unexpected)'
* fix(web): Improve update plugin error handling and support form data
1. Enhanced updatePlugin JavaScript function:
- Validates pluginId before sending request
- Checks response.ok before parsing JSON
- Better error logging with request/response details
- Handles both successful and error responses properly
2. Update endpoint now supports both JSON and form data:
- Similar to config endpoint, accepts plugin_id from query string or form
- Better error messages and debug logging
3. Prevent duplicate function definitions:
- Second updatePlugin definition checks if improved version exists
- Both definitions now have consistent error handling
Fixes: 400 BAD REQUEST 'Request body must be valid JSON' error
* fix(web): Show correct 'update' message instead of 'save' for plugin updates
The handlePluginUpdate function now:
1. Checks actual HTTP status code (not just event.detail.successful)
2. Parses JSON response to get server's actual message
3. Replaces 'save' with 'update' if message incorrectly says 'save'
Fixes: Update button showing 'saved successfully' instead of
'updated successfully'
* fix(web): Execute plugin updates immediately instead of queuing
Plugin updates are now executed directly (synchronously) instead of
being queued for async processing. This provides immediate feedback
to users about whether the update succeeded or failed.
Updates are fast git pull operations, so they don't need async
processing. The operation queue is reserved for longer operations
like install/uninstall.
Fixes: Update button not actually updating plugins (operations were
queued but users didn't see results)
* fix(web): Ensure toggleSection function is always available for collapsible headers
Moved toggleSection outside the initialization guard block so it's
always defined, even if the plugin config handlers have already been
initialized. This ensures collapsible sections in plugin config forms
work correctly.
Added debug logging to help diagnose if sections/icons aren't found.
Fixes: Collapsible headers in plugin config schema not collapsing
* fix(web): Improve toggleSection to explicitly show/hide collapsible content
Changed from classList.toggle() to explicit add/remove of 'hidden' class
based on current state. This ensures the content visibility is properly
controlled when collapsing/expanding sections.
Added better error checking and state detection for more reliable
collapsible section behavior.
* fix(web): Load plugin tabs on page load instead of waiting for plugin manager tab click
The stub's loadInstalledPlugins was an empty function, so plugin tabs
weren't loading until the plugin manager tab was clicked. Now the stub
implementation:
1. Tries to use global window.loadInstalledPlugins if available
2. Falls back to window.pluginManager.loadInstalledPlugins
3. Finally falls back to direct loading via loadInstalledPluginsDirectly
4. Always updates tabs after loading plugins
This ensures plugin navigation tabs are available immediately on page load.
Fixes: Plugin tabs only loading after clicking plugin manager tab
* fix(web): Ensure plugin navigation tabs load on any page regardless of active tab
Multiple improvements to ensure plugin tabs are always visible:
1. Stub's loadInstalledPluginsDirectly now waits for DOM to be ready
before updating tabs, using requestAnimationFrame for proper timing
2. Stub's init() now has a retry mechanism that periodically checks
if plugins have been loaded by plugins_manager.js and updates tabs
accordingly (checks for 2 seconds)
3. Full implementation's init() now properly handles async plugin loading
and ensures tabs are updated after loading completes, checking
window.installedPlugins first before attempting to load
4. Both stub and full implementation ensure tabs update using $nextTick
to wait for Alpine.js rendering cycle
This ensures plugin navigation tabs are visible immediately when the
page loads, regardless of whether the user is on overview, plugin manager,
or any other tab.
Fixes: Plugin tabs only appearing after clicking plugin manager tab
* fix(web): Fix restart display button not working
The initPluginsPage function was returning early before event listeners
were set up, making all the event listener code unreachable. Moved the
return statement to after all event listeners are attached.
This fixes the restart display button and all other buttons in the
plugin manager (refresh plugins, update all, search, etc.) that depend
on event listeners being set up.
Fixes: Restart Display button not working in plugin manager
* fix(web-ui): Improve categories field rendering for of-the-day plugin
- Add more explicit condition checking for additionalProperties objects
- Add debug logging specifically for categories field
- Add fallback handler for objects that don't match special cases (render as JSON textarea)
- Ensure categories section displays correctly with toggle cards instead of plain text
* fix(install): Prevent following broken symlinks during file ownership setup
- Add -P flag to find commands to prevent following symlinks when traversing
- Add -h flag to chown to operate on symlinks themselves rather than targets
- Exclude scripts/dev/plugins directory which contains development symlinks
- Fixes error when chown tries to dereference broken symlinks with extra LEDMatrix in path
* fix(scroll): Ensure scroll completes fully before switching displays
- Add display_width to total scroll distance calculation
- Scroll now continues until content is completely off screen
- Update scroll completion check to use total_scroll_width + display_width
- Prevents scroll from being cut off mid-way when switching to next display
* fix(install): Remove unsupported -P flag from find commands
- Remove -P flag which is not supported on all find versions
- Keep -h flag on chown to operate on symlinks themselves
- Change to {} \; syntax for better error handling
- Add error suppression to continue on broken symlinks
- Exclude scripts/dev/plugins directory to prevent traversal into broken symlinks
* docs(wifi): Add trailing newline to WiFi AP failover setup guide
* fix(web): Suppress non-critical socket errors and fix WiFi permissions script
- Add error filtering in web interface to suppress harmless client disconnection errors
- Downgrade 'No route to host' and broken pipe errors from ERROR to DEBUG level
- Fix WiFi permissions script to use mktemp instead of manual temp file creation
- Add cleanup trap to ensure temp files are removed on script exit
- Resolves permission denied errors when creating temp files during installation
* fix(web): Ensure plugin navigation tabs load on any page by dispatching events
The issue was that when plugins_manager.js loaded and called
loadInstalledPlugins(), it would set window.installedPlugins but the
Alpine.js component wouldn't know to update its tabs unless the plugin
manager tab was clicked.
Changes:
1. loadInstalledPlugins() now always dispatches a 'pluginsUpdated' event
when it sets window.installedPlugins, not just when plugin IDs change
2. renderInstalledPlugins() also dispatches the event and always updates
window.installedPlugins for reactivity
3. Cached plugin data also dispatches the event when returned
The Alpine component already listens for the 'pluginsUpdated' event in
its init() method, so tabs will now update immediately when plugins are
loaded, regardless of which tab is active.
Fixes: Plugin navigation tabs only loading after clicking plugin manager tab
* fix(web): Improve input field contrast in plugin configuration forms
Changed input backgrounds from bg-gray-800 to bg-gray-900 (darker) to
ensure high contrast with white text. Added placeholder:text-gray-400
for better placeholder text visibility.
Updated in both server-side template (plugin_config.html) and client-side
form generation (plugins_manager.js):
- Number inputs
- Text inputs
- Array inputs (comma-separated)
- Select dropdowns
- Textareas (JSON objects)
- Fallback inputs without schema
This ensures all form inputs have high contrast white text on dark
background, making them clearly visible and readable.
Fixes: White text on white background in plugin config inputs
* fix(web): Change plugin config input text from white to black
Changed all input fields in plugin configuration forms to use black text
on white background instead of white text on dark background for better
readability and standard form appearance.
Updated:
- Input backgrounds: bg-gray-900 -> bg-white
- Text color: text-white -> text-black
- Placeholder color: text-gray-400 -> text-gray-500
Applied to both server-side template and client-side form generation
for all input types (number, text, select, textarea).
* fix(web): Ensure toggleSection function is available for plugin config collapsible sections
Moved toggleSection function definition to an early script block so it's
available immediately when HTMX loads plugin configuration content. The
function was previously defined later in the page which could cause it
to not be accessible when inline onclick handlers try to call it.
The function toggles the 'hidden' class on collapsible section content
divs and rotates the chevron icon between right (collapsed) and down
(expanded) states.
Fixes: Plugin configuration section headers not collapsing/expanding
* fix(web): Fix collapsible section toggle to properly hide/show content
Updated toggleSection function to explicitly set display style in addition
to toggling the hidden class. This ensures the content is properly hidden
even if CSS specificity or other styles might interfere with just the
hidden class.
The function now:
- Checks both the hidden class and computed display style
- Explicitly sets display: '' when showing and display: 'none' when hiding
- Rotates chevron icon between right (collapsed) and down (expanded)
This ensures collapsible sections in plugin configuration forms properly
hide and show their content when the header is clicked.
Fixes: Collapsible section headers rotate chevron but don't hide content
* fix(web): Fix collapsible section toggle to work on first click
Simplified the toggle logic to rely primarily on the 'hidden' class check
rather than mixing it with computed display styles. When hiding, we now
remove any inline display style to let Tailwind's 'hidden' class properly
control the display property.
This ensures sections respond correctly on the first click, whether they're
starting in a collapsed or expanded state.
Fixes: Sections requiring 2 clicks to collapse
* fix(web): Ensure collapsible sections start collapsed by default
Added explicit display: none style to nested content divs in plugin config
template to ensure they start collapsed. The hidden class should handle this,
but adding the inline style ensures sections are definitely collapsed on
initial page load.
Sections now:
- Start collapsed (hidden) with chevron pointing right
- Expand when clicked (chevron points down)
- Collapse when clicked again (chevron points right)
This ensures a consistent collapsed initial state across all plugin
configuration sections.
* fix(web): Fix collapsible section toggle to properly collapse on second click
Fixed the toggle logic to explicitly set display: block when showing and
display: none when hiding, rather than clearing the display style. This
ensures the section state is properly tracked and the toggle works correctly
on both expand and collapse clicks.
The function now:
- When hidden: removes hidden class, sets display: block, chevron down
- When visible: adds hidden class, sets display: none, chevron right
This fixes the issue where sections would expand but not collapse again.
Fixes: Sections not collapsing on second click
* feat(web): Ensure plugin navigation tabs load automatically on any page
Implemented comprehensive solution to ensure plugin navigation tabs load
automatically without requiring a visit to the plugin manager page:
1. Global event listener for 'pluginsUpdated' - works even if Alpine isn't
ready yet, updates tabs directly when plugins_manager.js loads plugins
2. Enhanced stub's loadInstalledPluginsDirectly():
- Sets window.installedPlugins after loading
- Dispatches 'pluginsUpdated' event for global listener
- Adds console logging for debugging
3. Event listener in stub's init() method:
- Listens for 'pluginsUpdated' events
- Updates component state and tabs when events fire
4. Fallback timer:
- If plugins_manager.js hasn't loaded after 2 seconds, fetches
plugins directly via API
- Ensures tabs appear even if plugins_manager.js fails
5. Improved checkAndUpdateTabs():
- Better logging
- Fallback to direct fetch after timeout
6. Enhanced logging throughout plugin loading flow for debugging
This ensures plugin tabs are visible immediately on page load, regardless
of which tab is active or when plugins_manager.js loads.
Fixes: Plugin navigation tabs only loading after visiting plugin manager
* fix(web): Improve plugin tabs update logging and ensure immediate execution
Enhanced logging in updatePluginTabs() and _doUpdatePluginTabs() to help
debug why tabs aren't appearing. Changed debounce behavior to execute
immediately on first call to ensure tabs appear quickly.
Added detailed console logging with [FULL] prefix to track:
- When updatePluginTabs() is called
- When _doUpdatePluginTabs() executes
- DOM element availability
- Tab creation process
- Final tab count
This will help identify if tabs are being created but not visible, or if
the update function isn't being called at all.
Fixes: Plugin tabs loading but not visible in navigation bar
* fix(web): Prevent duplicate plugin tab updates and clearing
Added debouncing and duplicate prevention to stub's updatePluginTabs() to
prevent tabs from being cleared and re-added multiple times. Also checks
if tabs already match before clearing them.
Changes:
1. Debounce stub's updatePluginTabs() with 100ms delay
2. Check if existing tabs match current plugin list before clearing
3. Global event listener only triggers full implementation's updatePluginTabs
4. Stub's event listener only works in stub mode (before enhancement)
This prevents the issue where tabs were being cleared and re-added
multiple times in rapid succession, which could leave tabs empty.
Fixes: Plugin tabs being cleared and not re-added properly
* fix(web): Fix plugin tabs not rendering when plugins are loaded
Fixed _doUpdatePluginTabs() to properly use component's installedPlugins
instead of checking window.installedPlugins first. Also fixed the 'unchanged'
check to not skip when both lists are empty (first load scenario).
Changes:
1. Check component's installedPlugins first (most up-to-date)
2. Only skip update if plugins exist AND match (don't skip empty lists)
3. Retry if no plugins found (in case they're still loading)
4. Ensure window.installedPlugins is set when loading directly
5. Better logging to show which plugin source is being used
This ensures tabs are rendered when plugins are loaded, even on first page load.
Fixes: Plugin tabs not being drawn despite plugins being loaded
* fix(config): Fix array field parsing and validation for plugin config forms
- Added logic to detect and combine indexed array fields (text_color.0, text_color.1, etc.)
- Fixed array fields incorrectly stored as dicts with numeric keys
- Improved handling of comma-separated array values from form submissions
- Ensures array fields meet minItems requirements before validation
- Resolves 400 BAD REQUEST errors when saving plugin config with RGB color arrays
* fix(config): Improve array field handling and secrets error handling
- Use schema defaults when array fields don't meet minItems requirement
- Add debug logging for array field parsing
- Improve error handling for secrets file writes
- Fix arrays stored as dicts with numeric keys conversion
- Better handling of incomplete array values from form submissions
* fix(config): Convert array elements to correct types (numbers not strings)
- Fix array element type conversion when converting dicts to arrays
- Ensure RGB color arrays have integer elements, not strings
- Apply type conversion for both nested and top-level array fields
- Fixes validation errors: 'Expected type number, got str'
* fix(config): Fix array fields showing 'none' when value is null
- Handle None/null values in array field templates properly
- Use schema defaults when array values are None/null
- Fix applies to both Jinja2 template and JavaScript form generation
- Resolves issue where stock ticker plugin shows 'none' instead of default values
* fix(config): Add novalidate to plugin config form to prevent HTML5 validation blocking saves
- Prevents browser HTML5 validation from blocking form submission
- Allows custom validation logic to handle form data properly
- Fixes issue where save button appears unclickable due to invalid form controls
- Resolves problems with plugins like clock-simple that have nested/array fields
* feat(config): Add helpful form validation with detailed error messages
- Keep HTML5 validation enabled (removed novalidate) to prevent broken configs
- Add validatePluginConfigForm function that shows which fields fail and why
- Automatically expands collapsed sections containing invalid fields
- Focuses first invalid field and scrolls to it
- Shows user-friendly error messages with field names and specific issues
- Prevents form submission until all fields are valid
* fix(schema): Remove core properties from required array during validation
- Core properties (enabled, display_duration, live_priority) are system-managed
- SchemaManager now removes them from required array after injection
- Added default values for core properties (enabled=True, display_duration=15, live_priority=False)
- Updated generate_default_config() to ensure live_priority has default
- Resolves 186 validation issues, reducing to 3 non-blocking warnings (98.4% reduction)
- All 19 of 20 plugins now pass validation without errors
Documentation:
- Created docs/PLUGIN_CONFIG_CORE_PROPERTIES.md explaining core property handling
- Updated existing docs to reflect core property behavior
- Removed temporary audit files and scripts
* fix(ui): Improve button text contrast on white backgrounds
- Changed Screenshot button text from text-gray-700 to text-gray-900
- Added global CSS rule to ensure all buttons with white backgrounds use dark text (text-gray-900) for better readability
- Fixes contrast issues where light text on light backgrounds was illegible
* fix(ui): Add explicit text color to form-control inputs
- Added color: #111827 to .form-control class to ensure dark text on white backgrounds
- Fixes issue where input fields had white text on white background after button contrast fix
- Ensures all form inputs are readable with proper contrast
* docs: Update impact explanation and plugin config documentation
* docs: Improve documentation and fix template inconsistencies
- Add migration guide for script path reorganization (scripts moved to scripts/install/ and scripts/fix_perms/)
- Add breaking changes section to README with migration guidance
- Fix config template: set plugins_directory to 'plugins' to match actual plugin locations
- Fix test template: replace Jinja2 placeholders with plain text to match other templates
- Fix markdown linting: add language identifiers to code blocks (python, text, javascript)
- Update permission guide: document setgid bit (0o2775) for directory modes
- Fix example JSON: pin dependency versions and fix compatible_versions range
- Improve readability: reduce repetition in IMPACT_EXPLANATION.md
* feat(web): Make v3 interface production-ready for local deployment
- Phase 2: Real Service Integration
- Replace sample data with real psutil system monitoring (CPU, memory, disk, temp, uptime)
- Integrate display controller to read from /tmp/led_matrix_preview.png snapshot
- Scan assets/fonts directory and extract font metadata with freetype
- Phase 1: Security & Input Validation
- Add input validation module with URL, file upload, and config sanitization
- Add optional CSRF protection (gracefully degrades if flask-wtf missing)
- Add rate limiting (lenient for local use, prevents accidental abuse)
- Add file upload validation to font upload endpoint
- Phase 3: Error Handling
- Add global error handlers for 404, 500, and unhandled exceptions
- All endpoints have comprehensive try/except blocks
- Phase 4: Monitoring & Observability
- Add structured logging with JSON format support
- Add request logging middleware (tracks method, path, status, duration, IP)
- Add /api/v3/health endpoint with service status checks
- Phase 5: Performance & Caching
- Add in-memory caching system (separate module to avoid circular imports)
- Cache font catalog (5 minute TTL)
- Cache system status (10 second TTL)
- Invalidate cache on config changes
- All changes are non-blocking with graceful error handling
- Optional dependencies (flask-wtf, flask-limiter) degrade gracefully
- All imports protected with try/except blocks
- Verified compilation and import tests pass
* docs: Fix caching pattern logic flaw and merge conflict resolution plan
- Fix Basic Caching Pattern: Replace broken stale cache fallback with correct pattern
- Re-fetch cache with large max_age (31536000) in except block instead of checking already-falsy cached variable
- Fixes both instances in ADVANCED_PLUGIN_DEVELOPMENT.md
- Matches correct pattern from manager.py.template
- Fix MERGE_CONFLICT_RESOLUTION_PLAN.md merge direction
- Correct Step 1 to checkout main and merge plugins into it (not vice versa)
- Update commit message to reflect 'Merge plugins into main' direction
- Fixes workflow to match documented plugins → main merge
---------
Co-authored-by: Chuck <chuck@example.com>
* feat: Add configurable delay for music module skip when nothing playing
- Add 'skip_when_nothing_playing' and 'skip_delay_seconds' config options
- Implement timer-based skip logic that waits 2 seconds before skipping
- Reset timer when music resumes or display is deactivated
- Improve user experience by preventing jarring immediate skips
- Maintains backward compatibility with existing configurations
* feat: Add live priority support to music manager
- Add 'live_priority' and 'live_game_duration' config options for music
- Integrate music manager into live priority rotation system
- Add 'music_live' mode that participates in live priority takeover
- Music manager joins live priority rotation when actively playing
- Supports future plugin ecosystem with consistent live priority interface
- Maintains backward compatibility with existing music configurations
* feat: Add new music settings to web interface
- Add 'Skip When Nothing Playing' checkbox and delay configuration
- Add 'Enable Live Priority' checkbox and duration configuration
- Update music form JavaScript to save all new settings
- Users can now configure music skip behavior and live priority through web UI
* fix: Improve music live priority integration
- Enhanced music live content detection with detailed logging
- Modified live priority logic to properly handle music mode
- Added music to live priority sports collection when actively playing
- Updated live priority rotation to handle music mode correctly
- Improved live priority takeover logic for music manager
- Music now properly participates in live priority rotation when playing
* perf: Reduce logging frequency for music live priority
- Add timestamp-based throttling to music live priority logging
- Info messages now only appear every 30 seconds instead of every few milliseconds
- Debug messages are also throttled to reduce log spam
- Maintains visibility of important events while reducing noise
* Fix unreachable code bug in music live priority handling
- Moved music live priority logic outside the sports iteration loop where it was unreachable
- The 'if sport == music' block was never executed since 'music' wasn't in the sports list
- Consolidated music handling in its own separate section for cleaner code structure
- Added music configuration options to config template
- Maintained exact same functionality while eliminating dead code
* Fix music_live_game_duration setting not being used
- Modified get_current_duration() method to check for music live priority mode
- When music is in live priority mode, use configured live_game_duration setting
- Added proper config reading for music_live_game_duration during initialization
- Ensures music displays for the configured duration when in live priority mode
* Fix stale timestamp bug in live priority debug logging
- Fixed issue where debug log at line 1240 was using a stale current_time variable
- The current_time variable was conditionally reassigned in music live priority check
- This caused inconsistent log throttling behavior
- Now uses fresh current_time_for_debug variable for accurate throttling
- Add allow_unsafe_werkzeug=True parameter to socketio.run()
- Allows web UI to run cleanly when eventlet is not available
- Falls back gracefully to Werkzeug without production warnings
* Fix NBA leaderboard team ID field for logo fetching
- Add missing 'id' field to NBA team standings data structure
- Enables proper logo fetching from assets/sports/nba_logos/
- Fixes 'id' KeyError when creating NBA leaderboard images
- Includes diagnostic and test scripts for verification
* Add NBA logo downloader script and documentation
- download_nba_logos.py: Script to download all 30 NBA team logos from ESPN API
- README_NBA_LOGOS.md: Comprehensive documentation for the logo downloader
- Supports force re-download and quiet modes
- Downloads to assets/sports/nba_logos/ for leaderboard integration
* replace NBA Logos
* return NBA logo
* feat(config): optimize memory usage to prevent OOM killer
- Reduce brightness from 95 to 50 to lower power consumption
- Reduce refresh rate from 120Hz to 100Hz to reduce CPU/memory pressure
- Reduce background service workers from 3 to 1 per manager
- Change hardware mapping from adafruit-hat-pwm to adafruit-hat
- Expected memory savings: ~700MB reduction in background service usage
- Addresses SIGKILL errors caused by memory exhaustion on Raspberry Pi
Fixes: OOM killer terminating ledmatrix.service with status=9/KILL
* revert display brightness
* refactor(background-service): hardcode optimized settings and remove config blocks
- Hardcode background service to 1 worker in all managers
- Remove background_service config blocks from template
- Simplify configuration for users - no need to adjust system settings
- Memory optimization: ~700MB reduction in background service usage
- Settings: 1 worker, 30s timeout, 3 retries (hardcoded)
Files updated:
- src/base_classes/sports.py
- src/leaderboard_manager.py
- src/odds_ticker_manager.py
- src/soccer_managers.py
- src/milb_manager.py
- config/config.template.json
This prevents OOM killer errors by reducing memory usage from
15 background threads to 5 threads total across all managers.
* remove fallback in case of background service failure
- Added PIL import for Image and ImageFont classes
- Resolves NameError when running display_controller.py
- Fixes type hint for _load_and_resize_logo method
* feat(static-image): Add static image manager with web interface
- Create StaticImageManager class with image scaling and transparency support
- Add configuration options for display duration, zoom scale, and background color
- Integrate with display controller and web interface
- Add image upload functionality to web interface
- Support for various image formats with proper scaling
- Efficient image processing with aspect ratio preservation
- Ready for future scrolling feature implementation
* fix(static-image): Move display duration to main display_durations block
- Remove display_duration from static_image config section
- Update StaticImageManager to read duration from display.display_durations.static_image
- Remove display duration field from web interface form
- Update web interface JavaScript to not include display_duration in payload
- Follows same pattern as all other managers in the project
* feat(static-image): Add fit to display option
- Add fit_to_display checkbox to automatically scale images to fit display
- When enabled, images are scaled to fit display dimensions while preserving aspect ratio
- When disabled, manual zoom_scale control is available
- Update web interface with smart form controls (zoom scale disabled when fit to display is on)
- Prevents stretching or cropping - images are always properly fitted
- Default to fit_to_display=true for better user experience
* refactor(static-image): Remove zoom_scale and simplify to fit_to_display only
- Remove zoom_scale option entirely as it was confusing and redundant
- Simplify image processing to always fit to display dimensions
- Remove zoom_scale field from web interface
- Clean up JavaScript to remove zoom scale logic
- Images are now always properly fitted without stretching or cropping
- Much simpler and more intuitive user experience
- Add willReadFrequently: true to all getContext('2d') calls in templates/index_v2.html
- Fixes Canvas2D performance warning for multiple readback operations using getImageData
- Improves performance for LED matrix preview rendering with frequent pixel sampling
Update readme to advise installation of Rasbian Legacy (Bookworm) vs the brand new Trixie that has incompatible requirements
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
- Fix download_missing_logo call to use correct signature with Path parameter
- Add comprehensive permission error handling with helpful error messages
- Include instructions to run fix_assets_permissions.sh when permission errors occur
- Improve error logging for directory listing and logo access permission issues
- Add test script to verify permission error handling works correctly
Fixes: [Errno 13] Permission denied errors when downloading soccer team logos
* Consolidate MLB to completely use Baseball class
* typos
* add OT period number
* Add new live class and abstracts
* NCAA BB is consolidated
* MLB Working
* NCAA Hockey and NHL working
* didn't need wrapper function
* Add hockey shots on goal
* cleanup
---------
Co-authored-by: Alex Resnick <adr8282@gmail.com>
- Add self.games_list = [] to NHLRecentManager.__init__ method
- Fixes AttributeError when display method tries to access games_list
- Follows same pattern as other RecentManager classes
* feat(nhl): Integrate NHL manager with background service
- Fix NHL data source to use ESPN API instead of NHL.com API
- Add background service integration following NFL/NCAA pattern
- Add HTTP session with retry strategy and proper headers
- Add background fetch request tracking and callback handling
- Add synchronous fallback when background service disabled
- Update _fetch_nhl_api_data() to use background threading
- Simplify _fetch_data() method for consistency
- Add _fetch_nhl_api_data_sync() for direct API fallback
This brings NHL manager in line with other sports managers and resolves
the 'No events found in ESPN API response' error by using the correct
ESPN API endpoint that matches the existing parsing logic.
* update NHL
* fix(odds): Resolve incorrect sport and league parameters in _fetch_odds calls
- Fixed SportsCore._fetch_data() to call _fetch_odds(game) instead of _fetch_odds(game, sport_key, sport_key)
- Updated _fetch_odds method signature to accept only game parameter
- Added _fetch_odds_with_params helper method for sport-specific implementations
- Updated sport-specific managers to use correct sport and league parameters:
- NFL: football/nfl
- NCAA Football: football/college-football
- NCAA Hockey: hockey/mens-college-hockey
- Ensures odds are fetched with correct ESPN API endpoints
Fixes#79
* Fix odds and other things
* update hockey
* fix rankings
* update imports
* Fix Logo Cache
* Add show_favorite_team_only attribute
---------
Co-authored-by: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com>
Co-authored-by: Alex Resnick <adr8282@gmail.com>
* fix: Resolve permission errors when downloading sports logos
- Fix fix_assets_permissions.sh to use correct directory name 'ncaa_logos' instead of 'ncaa_fbs_logos'
- Add comprehensive permission checking in logo downloader
- Improve error handling with specific permission error messages
- Add write access testing before attempting logo downloads
- Provide clear instructions to run permission fix script when errors occur
Fixes: [Errno 13] Permission denied errors when downloading team logos like SELA.png
* fix: Update first_time_install.sh to use correct ncaa_logos directory
- Fix manual permission setting section to use 'ncaa_logos' instead of 'ncaa_fbs_logos'
- Ensures consistency across all installation scripts
- Prevents permission issues during first-time installation
* fix: Update all remaining references from ncaa_fbs_logos to ncaa_logos
- Fix README.md directory reference
- Update wiki documentation files (MANAGER_GUIDE_COMPREHENSIVE.md, TEAM_ABBREVIATIONS_AND_LEAGUE_SLUGS.md)
- Fix test files (save_missing_teams.py, test_ranking_toggle.py, test_leaderboard_simple.py)
- Update missing_team_logos.txt with correct directory paths
Ensures complete consistency across the entire project for NCAA logo directory naming.
* fix(web-ui): Use config display dimensions instead of hardcoded 128x32 in Live Display Preview
- Updated /api/display/current endpoint to calculate display dimensions from config
- Modified HTML template to use config dimensions as fallbacks instead of hardcoded 128x32
- Display preview now shows correct resolution based on cols*chain_length x rows*parallel
- Maintains backward compatibility with existing API responses
* feat(web-ui): Add missing NCAAM Hockey sports manager to web interface
- Added NCAAM Hockey import and OnDemandRunner support
- Updated sports configuration UI to include NCAAM Hockey
- Fixed MLB and MiLB config keys to match template (_scoreboard suffix)
- All sports managers now properly represented in web interface:
- NFL, MLB, MiLB, NHL, NBA, NCAA FB, NCAA Baseball, NCAAM Basketball, NCAAM Hockey, Soccer
- Maintains backward compatibility with existing configurations
* fix(web-ui): Improve on-demand button error handling and prevent crashes
- Enhanced error handling in OnDemandRunner with better logging and fallback modes
- Added robust display manager initialization with fallback support
- Improved error reporting via WebSocket to client for real-time feedback
- Added input validation for on-demand mode parameters
- Enhanced client-side error handling with better user notifications
- Added safety checks to prevent multiple on-demand instances
- Fixed display manager initialization issues that caused crashes
- Improved error recovery and graceful degradation
* feat: Add AP Top 25 dynamic teams feature
- Add DynamicTeamResolver for resolving AP_TOP_25, AP_TOP_10, AP_TOP_5 to actual team abbreviations
- Integrate dynamic team resolution into SportsCore base class
- Add comprehensive test suite for dynamic team functionality
- Update config template with AP_TOP_25 example
- Add complete documentation for the new feature
Features:
- Automatic weekly updates of AP Top 25 rankings
- 1-hour caching for performance optimization
- Support for AP_TOP_25, AP_TOP_10, AP_TOP_5 dynamic teams
- Seamless integration with existing favorite teams system
- Comprehensive error handling and edge case support
Tests:
- Unit tests for core dynamic team resolution
- Integration tests for configuration scenarios
- Performance tests for caching functionality
- Edge case tests for unknown dynamic teams
All tests passing with 100% success rate.
* docs: Update wiki submodule with AP Top 25 documentation
- Add comprehensive documentation for AP Top 25 dynamic teams feature
- Include usage examples, configuration guides, and troubleshooting
- Update submodule reference to include new documentation
* feat: Add AP_TOP_25 support to odds ticker
- Integrate DynamicTeamResolver into OddsTickerManager
- Resolve dynamic teams for all enabled leagues during initialization
- Add comprehensive logging for dynamic team resolution
- Support AP_TOP_25, AP_TOP_10, AP_TOP_5 in odds ticker
- Add test suite for odds ticker dynamic teams integration
Features:
- Odds ticker now automatically resolves AP_TOP_25 to current top 25 teams
- Shows odds for all current AP Top 25 teams automatically
- Updates weekly when rankings change
- Works seamlessly with existing favorite teams system
- Supports mixed regular and dynamic teams
Tests:
- Configuration integration tests
- Multiple league configuration tests
- Edge case handling tests
- All tests passing with 100% success rate
* fix(odds-ticker): Reduce log spam from insufficient time warnings
- Add _insufficient_time_warning_logged flag to prevent repeated warnings
- Log insufficient time warning only once per display session
- Reset warning flag when starting new display or updating data
- Maintain debug logging for scroll position resets to aid debugging
This addresses the frequent 'Not enough time to complete content display'
warnings that were flooding the logs every few milliseconds.
* fix(leaderboard): Reduce log spam from progress and FPS logging
- Change progress logging from every 50 pixels to every 5 seconds
- Increase FPS logging interval from 10 seconds to 30 seconds
- Add progress_log_interval and last_progress_log_time variables
- Reset progress log timer when starting new display sessions or updating data
- Maintain debug capability while significantly reducing log volume
This addresses the frequent 'Leaderboard progress' and 'Leaderboard FPS'
messages that were flooding the logs during leaderboard scrolling.
* fix(music): Reduce log spam from track update logging
- Add throttling mechanism for track update logging
- Log track updates only when track title changes or after 5 seconds
- Track last_logged_track_title and last_track_log_time to prevent spam
- Maintain debug logging for throttled updates
- Apply throttling to both regular updates and first valid data logging
This addresses the frequent 'Track info updated' messages that were
flooding the logs every few hundred milliseconds for the same track.
* fix(logo-downloader): Fix TA&M filename normalization issue
- Add special case for TA&M to keep as TA&M instead of converting to TAANDM
- This fixes the issue where code was looking for TAANDM.png instead of TA&M.png
- The actual logo file exists as TA&M.png, so normalization was causing file not found errors
- Prevents unnecessary download attempts and permission errors for existing files
Fixes the error: 'Logo not found for TA&M at assets/sports/ncaa_logos/TAANDM.png'
* fix(logo-downloader): Implement robust filename variation handling
- Add get_logo_filename_variations() method to handle multiple filename formats
- Update _load_and_resize_logo() to try filename variations before downloading
- Handles cases like TA&M.png vs TAANDM.png gracefully
- Maintains backward compatibility with existing normalized filenames
- Prevents issues with special characters in filenames while supporting existing files
This addresses the ampersand filename issue more robustly by:
1. First trying the original filename (TA&M.png)
2. Falling back to normalized filename (TAANDM.png) if needed
3. Only attempting downloads if no variations exist
* fix(leaderboard): Reduce log spam from end reached messages
- Add _end_reached_logged flag to prevent repeated end reached warnings
- Log 'Leaderboard reached end' and 'scrolling stopped' messages only once per display session
- Maintain debug logging for throttled messages to aid debugging
- Reset flag when starting new display sessions or updating data
- Apply same throttling pattern used in odds ticker manager
This addresses the frequent 'Leaderboard reached end' and 'scrolling stopped'
messages that were flooding the logs every few milliseconds when at the end.
* Fix leaderboard scrolling performance after PR #39 merge
- Restore leaderboard background updates that were accidentally removed
- Fix duration method call from get_dynamic_duration() back to get_duration()
- Restore proper fallback duration (600s instead of 60s) for leaderboard
- Add back sports manager updates that feed data to leaderboard
- Fix leaderboard defer_update priority to prevent scrolling lag
These changes restore the leaderboard's dynamic duration calculation
and ensure it gets proper background updates for smooth scrolling.
* Apply PR #60 leaderboard performance optimizations
- Change scroll_delay from 0.05s to 0.01s (100fps instead of 20fps)
- Remove conditional scrolling logic - scroll every frame for smooth animation
- Add FPS tracking and logging for performance monitoring
- Restore high-framerate scrolling that was working before PR #39 merge
These changes restore the smooth leaderboard scrolling performance
that was achieved in PR #60 but was lost during the PR #39 merge.
* Fix critical bugs identified in PR #39 review
- Fix record filtering logic bug: change away_record == set to away_record in set
- Fix incorrect sport specification: change 'nfl' to 'ncaa_fb' for NCAA Football data requests
- These bugs were causing incorrect data display and wrong sport data fetching
Addresses issues found by cursor bot in PR #39 review:
- Record filtering was always evaluating to False
- NCAA Football was fetching NFL data instead of college football data
* Enhance cache clearing implementation from PR #39
- Add detailed logging to cache clearing process for better visibility
- Log cache clearing statistics (memory entries and file count)
- Improve startup logging to show cache clearing and data refetch process
- Addresses legoguy1000's comment about preventing stale data issues
This enhances the cache clearing implementation that was added in PR #39
to help prevent legacy cache issues and stale data problems.
* continuing on base_classes - added baseball and api extractor since we don't use ESPN api for all sports
* tests
* fix missing duration
* ensure milb, mlb, ncaa bb are all using new baseball base class properly
* cursor rule to help with PR creation
* fix image call
* fix _scoreboard suffix on milb, MLB
* rebase
* Update NFL and NCAA FB fetch
* update FB updates
* kinda working, kinda broken
* Fixed and update loggers
* move to individual files
* timeout updates
* seems to work well
* Leaderboard overestimates time
* ignore that
* minor syntax updates
* More consolidation but i broke something
* fixed
* Hockey seems to work
* Fix my changes to logo downloader
* even more consolidation
* fixes
* more cleanup
* inheritance stuff
* Change football to ESPN down text, it does what ur already doing. Change color to red on Red ZOne
* Fix leaderboard
* Update football.py
Signed-off-by: Alex Resnick <adr8292@gmail.com>
* Minor fixes
* don't want that
* background fetch
* whoops
---------
Signed-off-by: Alex Resnick <adr8292@gmail.com>
Co-authored-by: Alex Resnick <adr8282@gmail.com>
Co-authored-by: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com>
* Fix duplicate/redundant caching issue
- Add get_background_cached_data() and is_background_data_available() methods to CacheManager
- Update sports managers to use background service cache instead of individual caching
- Ensure consistent cache key generation between background service and managers
- Eliminate redundant API calls by making Recent/Upcoming managers cache consumers
- Fix cache miss issues where TTL < update interval
This addresses GitHub issue #57 by implementing a cleaner caching architecture
where the background service is the primary data source and managers are cache consumers.
* Update remaining sports managers to use background service cache
- Update NHL managers to use background service cache
- Update NCAA Football managers to use background service cache
- Update NCAA Hockey managers to use background service cache
- Update MLB managers to use background service cache for Recent/Upcoming
All sports managers now use the new caching architecture to eliminate
duplicate caching and redundant API calls.
* cache improvements
* updated cache manager
* Fix leaderboard gap to use display width instead of hardcoded values
- Replace hardcoded spacing (40px) with display_manager.matrix.width
- Update gap calculation to use display width for blank screen simulation
- Fix display width logging to show correct value
- Ensures gap between league rotations matches actual display width
* Add display width gap to news manager
- Add display width gap at the beginning of news content
- Update total_scroll_width calculation to include display width gap
- Modify create_scrolling_image to draw text after display width gap
- Ensures news starts with blank screen period matching display width
- Removed duplicate create_scrolling_image method
* add Live, Recent, Upcoming toggles to display modes on website
* Fix leaderboard timing issues with comprehensive improvements
- Add maximum display time cap (120s) to prevent hanging
- Implement dynamic scroll speed tracking with runtime measurements
- Simplify complex timing logic that was causing hangs
- Add enhanced progress tracking and logging
- Add configurable safety buffer (10s)
- Update config template with new timing options
- Add comprehensive test suite for timing logic
Fixes the 30-second hanging issue reported in PR #53 by providing
multiple layers of protection against time overestimation.
* Simplify leaderboard timing to use long timeout with exception-based ending
- Remove complex dynamic duration calculations
- Remove safety buffer complexity
- Remove scroll speed tracking and measurements
- Use simple 10-minute timeout (600s) for both display_duration and max_display_time
- Let content determine when display is complete via existing StopIteration logic
- Update display controller to use simplified duration approach
- Clean up config template to remove unused timing settings
This approach is much more reliable than trying to predict content duration
and eliminates the hanging issues reported in PR #53.
* Fix configuration structure to use centralized display_durations
- Remove redundant display_duration from leaderboard section
- Use main display_durations.leaderboard (300s) for fixed duration mode
- Update leaderboard manager to read from centralized config
- Increase leaderboard default duration from 60s to 300s for better content coverage
- Maintain dynamic_duration option for user choice between fixed/dynamic modes
- Add comprehensive scroll behavior analysis and testing
This completes the leaderboard timing improvements with proper config structure.
* scroll every frame to be smoother like the stock ticker instead of waiting per subsecond
* leaderboard block api calls while scrolling
* leaderboard debugging
* added leaderboard fps logging
* leaderboard frame control and optimizations
* background update memory leak for scrolling text found and first solution applied
* tuning scroll speeds
* working display scrolls
* revert scroll delay to 0.01 (about 100fps)
* revert min duration of leaderboard
* remove onetime test scripts
- Add all sports managers to background updates in display_controller.py
- Add all sports managers to deferred updates when scrolling
- Fix hardcoded update intervals in NHL, NBA, NCAA Basketball, and NCAA Hockey managers
- Use config values for recent_update_interval and upcoming_update_interval
- Ensure all sports managers update continuously in background
- Only NFL managers had the season fetching issue, others were already optimized
- Add proper timezone support to _check_schedule() method
- Use configured timezone instead of system local time
- Add pytz import for timezone handling
- Ensures schedule respects America/Chicago timezone setting
- Fixes issue where display wouldn't turn off at configured end_time
* Fix Spotify flashing display issue
- Implement proper significant change detection for Spotify polling
- Only trigger significant changes for title, artist, album_art_url, is_playing changes
- Progress updates (progress_ms) are now non-significant and won't cause flashing
- Matches the logic used for YouTube Music polling
- Prevents unnecessary album art re-fetching and display clearing every 2 seconds
* change default leaderboard scroll speed
- Added config_type='main' to FormData when submitting display form
- Added proper HTTP error handling to catch non-JSON responses
- This fixes the 'Unexpected token <' JSON parsing error when saving display settings
- Implement proper significant change detection for Spotify polling
- Only trigger significant changes for title, artist, album_art_url, is_playing changes
- Progress updates (progress_ms) are now non-significant and won't cause flashing
- Matches the logic used for YouTube Music polling
- Prevents unnecessary album art re-fetching and display clearing every 2 seconds
* Fix NFL live update interval not being respected
- Add NFL managers to _update_modules() method in display_controller.py
- NFL managers now update continuously in background instead of only when displayed
- Fixes issue where NFL live data would only fetch once and never update
- NFL live manager now properly respects 30-second live_update_interval setting
- Also add NFL managers to deferred updates when display is scrolling
* Fix NFL Recent and Upcoming managers to use correct update intervals
- NFL Recent Manager now uses recent_update_interval (3600 seconds) instead of hardcoded 300
- NFL Upcoming Manager now uses upcoming_update_interval (3600 seconds) instead of hardcoded 300
- Ensures proper update intervals: Live=30s, Recent=3600s, Upcoming=3600s
- All managers respect their respective config intervals
* Fix NCAAFB managers to use correct update intervals and add to background updates
- NCAAFB Recent Manager now uses recent_update_interval (3600 seconds) instead of hardcoded 300
- NCAAFB Upcoming Manager now uses upcoming_update_interval (3600 seconds) instead of hardcoded 300
- Add NCAAFB managers to _update_modules() method in display_controller.py
- NCAAFB managers now update continuously in background instead of only when displayed
- Add NCAAFB managers to deferred updates when display is scrolling
- Ensures proper update intervals: Live=30s, Recent=3600s, Upcoming=3600s
* Fix NFL live manager to use cached data for fresh updates
- Change NFL Live Manager to use cache=True instead of cache=False
- NFL Live Manager was bypassing cache and not getting fresh data from background service
- Background service fetches and caches data, but live manager was ignoring the cache
- This fixes the issue where NFL live display shows stale data despite successful background fetches
- Live manager now gets fresh data from the background service cache
* Fix NFL live manager to force fresh data fetch when update interval passes
- NFL Live Manager now forces fresh fetch when update interval (30s) has passed
- Uses cache for immediate response but bypasses cache for fresh updates
- Adds logging to show when fresh fetch is forced
- This ensures live game data updates every 30 seconds with fresh scores/clock
- Fixes issue where live display was showing stale data despite background fetches
* Fix NFL live manager to always fetch fresh data from API
- NFL Live Manager now always fetches fresh data directly from ESPN API
- No cache usage for live games - ensures real-time scores and clock updates
- Live games should always show the latest information, not cached data
- This ensures live display updates every 30 seconds with current game state
* Fix NFL live manager to use synchronous API fetch for real-time updates
- NFL Live Manager now uses _fetch_nfl_api_data_sync() for direct API calls
- Bypasses background service and cache for immediate fresh data
- Ensures live games get real-time scores and clock updates every 30 seconds
- Direct API calls are faster and more reliable for live game data
* Fix NFL live manager to fetch only current games instead of entire season
- Create _fetch_current_nfl_games() method for live updates
- Only fetches current week's games, not entire 2025 season (335 events)
- Much faster API calls - only gets relevant live games
- Reduces API load and improves performance for live updates
- NFL Live Manager now fetches ~10-15 current games instead of 335 season games
* ensure live games aren't pulling whole season
* bugbot catches
* Fix NCAAFB ranking display issue
- Remove duplicate ranking system that was drawing rankings behind team logos
- Old system (_get_rank) was drawing rankings at top of logos
- New system (_fetch_team_rankings) correctly draws rankings in bottom corners
- Remove old ranking calls from live, recent, and upcoming game drawing functions
- Remove unnecessary _fetch_rankings() calls from update methods
- Rankings now only appear in designated corner positions, not overlapping logos
Fixes issue where team rankings/betting lines were being drawn behind
team logos instead of replacing team records in the corners.
* Add missing show_ranking and show_records options to NCAAFB web UI
- Add show_ranking option to NCAAFB scoreboard config template
- Add show_records and show_ranking toggle switches to NCAAFB web UI
- Update JavaScript form collection to include new fields
- Users can now control whether to show team records or rankings via web interface
This completes the fix for NCAAFB ranking display - users can now enable
show_ranking in the web UI to see AP Top 25 rankings instead of team records.
* Implement Background Threading for Season Data Fetching
Phase 1: Background Season Data Fetching - COMPLETED
Key Features:
- Created BackgroundDataService class with thread-safe operations
- Implemented automatic retry logic with exponential backoff
- Modified NFL manager to use background service
- Added immediate partial data return for non-blocking display
- Comprehensive logging and statistics tracking
Performance Benefits:
- Main display loop no longer blocked by API calls
- Season data always fresh with background updates
- Better user experience during data fetching
Files Added/Modified:
- src/background_data_service.py (NEW)
- src/nfl_managers.py (updated)
- config/config.template.json (updated)
- test_background_service.py (NEW)
- BACKGROUND_SERVICE_README.md (NEW)
* Fix data validation issues in background service
- Add comprehensive data structure validation in NFL managers
- Handle malformed events gracefully with proper error logging
- Validate cached data format and handle legacy formats
- Add data validation in background service response parsing
- Fix TypeError: string indices must be integers, not 'str'
This fixes the error where events were being treated as strings
instead of dictionaries, causing crashes in recent/upcoming games.
* Phase 2: Apply Background Service to Major Sport Managers
✅ Applied background service support to:
- NCAAFB Manager (College Football)
- NBA Manager (Basketball)
- NHL Manager (Hockey)
- MLB Manager (Baseball)
🔧 Key Features Added:
- Background service initialization for each sport
- Configurable workers, timeouts, and retry settings
- Graceful fallback when background service is disabled
- Comprehensive logging for monitoring
⚙️ Configuration Updates:
- Added background_service config section to NBA
- Added background_service config section to NHL
- Added background_service config section to NCAAFB
- Each sport can independently enable/disable background service
📈 Performance Benefits:
- Season data fetching no longer blocks display loops
- Immediate response with cached/partial data
- Background threads handle heavy API calls
- Better responsiveness across all supported sports
Next: Apply to remaining managers (MiLB, Soccer, etc.)
* Fix Python compatibility issue in BackgroundDataService shutdown
🐛 Bug Fix:
- Fixed TypeError in ThreadPoolExecutor.shutdown() for older Python versions
- Added try/catch to handle timeout parameter compatibility
- Fallback gracefully for Python < 3.9 that doesn't support timeout parameter
🔧 Technical Details:
- ThreadPoolExecutor.shutdown(timeout=) was added in Python 3.9
- Older versions only support shutdown(wait=)
- Added compatibility layer with proper error handling
✅ Result:
- No more shutdown exceptions on older Python versions
- Graceful degradation for different Python environments
- Maintains full functionality on newer Python versions
* Phase 2 Complete: Background Service Applied to All Sport Managers
🎉 MAJOR MILESTONE: Complete Background Service Rollout
✅ All Sport Managers Now Support Background Service:
- MiLB Manager (Minor League Baseball)
- Soccer Manager (Multiple leagues: Premier League, La Liga, etc.)
- Leaderboard Manager (Multi-sport standings)
- Odds Ticker Manager (Live betting odds)
🔧 Technical Implementation:
- Background service initialization in all managers
- Configurable workers, timeouts, and retry settings
- Graceful fallback when background service is disabled
- Comprehensive logging for monitoring and debugging
- Thread-safe operations with proper error handling
⚙️ Configuration Support Added:
- MiLB: background_service config section
- Soccer: background_service config section
- Leaderboard: background_service config section
- Odds Ticker: background_service config section
- Each manager can independently enable/disable background service
📈 Performance Benefits Achieved:
- Non-blocking data fetching across ALL sport managers
- Immediate response with cached/partial data
- Background threads handle heavy API calls
- Significantly improved responsiveness
- Better user experience during data loading
🚀 Production Ready:
- All major sport managers now support background threading
- Comprehensive configuration options
- Robust error handling and fallback mechanisms
- Ready for production deployment
Next: Phase 3 - Advanced features (priority queuing, analytics)
* Update wiki submodule with Background Service documentation
📚 Wiki Documentation Added:
- Complete Background Service Guide with architecture diagrams
- Configuration examples and best practices
- Performance benefits and troubleshooting guide
- Migration guide and advanced features
🔧 Navigation Updates:
- Added to sidebar under Technical section
- Updated home page with performance section
- Highlighted as NEW feature with ⚡ icon
The wiki now includes comprehensive documentation for the new
background threading system that improves performance across
all sport managers.
* Fix CacheManager constructor in test script
🐛 Bug Fix:
- Fixed CacheManager initialization in test_background_service.py
- CacheManager no longer takes config_manager parameter
- Updated constructor call to match current implementation
✅ Result:
- Test script now works with current CacheManager API
- Background service testing can proceed without errors
* Move test_background_service.py to test/ directory
📁 Organization Improvement:
- Moved test_background_service.py from root to test/ directory
- Updated import paths to work from new location
- Fixed sys.path to correctly reference src/ directory
- Updated imports to use relative paths
🔧 Technical Changes:
- Changed sys.path from 'src' to '../src' (go up from test/)
- Updated imports to remove 'src.' prefix
- Maintains all functionality while improving project structure
✅ Benefits:
- Better project organization
- Test files properly grouped in test/ directory
- Cleaner root directory structure
- Follows standard Python project layout
* Remove old test_background_service.py from root directory
📁 Cleanup:
- Removed test_background_service.py from root directory
- File has been moved to test/ directory for better organization
- Maintains clean project structure
* Fix NCAA FB team ranking display functionality
- Add missing _fetch_team_rankings() calls to all update methods (live, recent, upcoming)
- Add ranking display logic to live manager scorebug layout
- Remove unused old _fetch_rankings() method and top_25_rankings variable
- Rankings now properly display as #X format when show_ranking is enabled
- Fixes non-functional ranking feature despite existing UI and configuration options
* Fix NCAAFB ranking display issue
- Remove duplicate ranking system that was drawing rankings behind team logos
- Old system (_get_rank) was drawing rankings at top of logos
- New system (_fetch_team_rankings) correctly draws rankings in bottom corners
- Remove old ranking calls from live, recent, and upcoming game drawing functions
- Remove unnecessary _fetch_rankings() calls from update methods
- Rankings now only appear in designated corner positions, not overlapping logos
Fixes issue where team rankings/betting lines were being drawn behind
team logos instead of replacing team records in the corners.
* Add missing show_ranking and show_records options to NCAAFB web UI
- Add show_ranking option to NCAAFB scoreboard config template
- Add show_records and show_ranking toggle switches to NCAAFB web UI
- Update JavaScript form collection to include new fields
- Users can now control whether to show team records or rankings via web interface
This completes the fix for NCAAFB ranking display - users can now enable
show_ranking in the web UI to see AP Top 25 rankings instead of team records.
- All missing PRs (#15, #17, #35, #36) are now in both main and development
- Latest development improvements merged to main
- Resolved conflicts by keeping development branch merged content
- Added NCAA Hockey support with new manager and logos
- Added emulator support with requirements file
- Added NCAA FB AP top 25 rankings functionality
- Added NCAA FB logo download capability
- Resolved conflicts by keeping development branch improvements while adding missing features
# Cursor Helper Files for LEDMatrix Plugin Development
This directory contains Cursor-specific helper files to assist with plugin development in the LEDMatrix project.
## Files Overview
### `.cursorrules`
Comprehensive rules file that Cursor uses to understand plugin development patterns, best practices, and workflows. This file is automatically loaded by Cursor and helps guide AI-assisted development.
Based on audit results showing 186 issues across 20 plugins.
## Overview
Three priority fixes identified from audit:
1.**Priority 1 (HIGH)**: Remove core properties from required array - will fix ~150 issues
2.**Priority 2 (MEDIUM)**: Verify default merging logic - will fix remaining required field issues
3.**Priority 3 (LOW)**: Calendar plugin schema cleanup - will fix 3 extra field warnings
## Priority 1: Remove Core Properties from Required Array
### Problem
Core properties (`enabled`, `display_duration`, `live_priority`) are system-managed but listed in schema `required` arrays. SchemaManager injects them into properties but doesn't remove them from `required`, causing validation failures.
### Solution
**File**: `src/plugin_system/schema_manager.py`
**Location**: `validate_config_against_schema()` method, after line 295
### Implementation Steps
1.**Add code to remove core properties from required array**:
```python
# After injecting core properties (around line 295), add:
# Remove core properties from required array (they're system-managed)
if "required" in enhanced_schema:
core_prop_names = list(core_properties.keys())
enhanced_schema["required"] = [
field for field in enhanced_schema["required"]
if field not in core_prop_names
]
```
2. **Add logging for debugging** (optional but helpful):
```python
if "required" in enhanced_schema and core_prop_names:
removed_from_required = [
field for field in enhanced_schema.get("required", [])
if field in core_prop_names
]
if removed_from_required and plugin_id:
self.logger.debug(
f"Removed core properties from required array for {plugin_id}: {removed_from_required}"
)
```
3. **Test the fix**:
- Run audit script: `python scripts/audit_plugin_configs.py`
- Expected: Issue count drops from 186 to ~30-40
- All "enabled" related errors should be eliminated
### Expected Outcome
- All 20 plugins should no longer fail validation due to missing `enabled` field
Some plugins have required fields with defaults that should be applied before validation. Need to verify the default merging happens correctly and handles nested objects.
### Solution
**File**: `web_interface/blueprints/api_v3.py`
**Location**: `save_plugin_config()` method, around lines 3218-3221
### Implementation Steps
1. **Review current default merging logic**:
- Check that `merge_with_defaults()` is called before validation (line 3220)
- Verify it's called after preserving enabled state but before validation
This guide provides comprehensive instructions for creating, running, and loading plugins in the LEDMatrix project.
## Table of Contents
1. [Plugin System Overview](#plugin-system-overview)
2. [Creating a New Plugin](#creating-a-new-plugin)
3. [Running Plugins](#running-plugins)
4. [Loading Plugins](#loading-plugins)
5. [Plugin Development Workflow](#plugin-development-workflow)
6. [Testing Plugins](#testing-plugins)
7. [Troubleshooting](#troubleshooting)
---
## Plugin System Overview
The LEDMatrix project uses a plugin-based architecture where all display functionality (except core calendar) is implemented as plugins. Plugins are dynamically loaded from the `plugins/` directory and integrated into the display rotation.
- **Automatic Version Management**: Version bumping is handled automatically via the pre-push git hook - no manual version bumping is required for normal development workflows
- **GitHub as Source of Truth**: Plugin store always fetches latest versions from GitHub (releases/tags/manifest/commit)
- **Pre-Push Hook**: Automatically bumps patch version and creates git tags when pushing code changes
- The hook is self-contained (no external dependencies) and works on any dev machine
- Installation: Copy the hook from LEDMatrix repo to your plugin repo:
-`plugins.json` registry at `https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugins/main/plugins.json`
- Store manager (`src/plugin_system/store_manager.py`) handles install/update/uninstall
- Monorepo plugins are installed via ZIP extraction (no `.git` directory)
- Update detection for monorepo plugins uses version comparison (manifest version vs registry latest_version)
- Plugin configs stored in `config/config.json`, NOT in plugin directories — safe across reinstalls
- Third-party plugins can use their own repo URL with empty `plugin_path`
## Common Pitfalls
- paho-mqtt 2.x needs `callback_api_version=mqtt.CallbackAPIVersion.VERSION1` for v1 compat
- BasePlugin uses `get_logger()` from `src.logging_config`, not standard `logging.getLogger()`
- When modifying a plugin in the monorepo, you MUST bump `version` in its `manifest.json` and run `python update_registry.py` — otherwise users won't receive the update
Tom-Thumb.bdf is included in this directory under [MIT license](http://vt100.tarunz.org/LICENSE). Tom-thumb.bdf was created by [@robey](http://twitter.com/robey) and originally published at https://robey.lag.net/2010/01/23/tiny-monospace-font.html
The texguire-27.bdf font was created using the [otf2bdf] tool from the TeX Gyre font.
```
The texgyre-27.bdf font was created using the [otf2bdf] tool from the TeX Gyre font.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.