From 698dcdd1db801b9b299fcb266d4f078e6ea7d503 Mon Sep 17 00:00:00 2001 From: Jean Mosquea Date: Sat, 15 Aug 2026 22:54:06 -0400 Subject: [PATCH 1/3] Tighten agent harness and secret handling across plugins. Refresh CLAUDE.md/AGENTS.md for cold-start agent work, ignore OAuth artifacts, and mask MQTT/API secret fields in the web UI with matching version bumps. Co-authored-by: Cursor --- .cursorrules | 160 +---- AGENTS.md | 42 ++ CLAUDE.md | 555 +++++++----------- plugins.json | 12 +- plugins/birdnet-go/config_schema.json | 3 +- plugins/birdnet-go/manifest.json | 10 +- plugins/calendar/.gitignore | 5 + plugins/calendar/manifest.json | 8 +- plugins/ledmatrix-weather/config_schema.json | 1 + plugins/ledmatrix-weather/manifest.json | 12 +- plugins/mqtt-notifications/config_schema.json | 3 +- plugins/mqtt-notifications/manifest.json | 8 +- plugins/on-air/config_schema.json | 1 + plugins/on-air/manifest.json | 10 +- plugins/pomodoro-timer/config_schema.json | 1 + plugins/pomodoro-timer/manifest.json | 14 +- 16 files changed, 346 insertions(+), 499 deletions(-) create mode 100644 AGENTS.md diff --git a/.cursorrules b/.cursorrules index fc2acd17..6eb1b849 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,150 +1,24 @@ # LEDMatrix Plugin Development Rules -## Monorepo Structure +**Source of truth for agents:** `AGENTS.md` → `CLAUDE.md`. +Human deep-dive: `docs/plugin-development/`. -All official plugins live in `plugins//` within this repository. -The registry file `plugins.json` is the source of truth for the Plugin Store. +Do not duplicate long API catalogs here. If a rule isn’t in `CLAUDE.md`, add it +there (session-memory / promote), not as a third copy. -**Full references:** `CLAUDE.md` (dense) and `docs/plugin-development/` (deep-dive -human guide) cover the plugin/core API, advanced features, styling, and adaptive -layout. Consult them before non-trivial changes. +## Hard rules (summary) -## Plugin Version Management +1. Bump `manifest.json` `version` + top `versions[]` entry on every plugin change. +2. Never hand-edit `plugins.json` — pre-commit / `update_registry.py` only. +3. Fetch in `update()`, draw in `display()`; namespace cache keys by plugin id. +4. Plugin-unique names for deferred/subpackage modules (`check_module_collisions.py`). +5. Pass the core safety harness (no crash / no overflow). Classic design sizes: + 64×32, 128×32, 128×64, 256×32 (CI may test more — see docs topic 07). +6. No secrets in git. Real values stay on the Pi/core runtime + (`config_secrets.json`); this repo keeps templates only. OAuth files + (`credentials.json`, `token.pickle`) are gitignored. -### When Making Changes to Any Plugin +Install hook: `cp scripts/pre-commit .git/hooks/pre-commit` -**ALWAYS follow this exact sequence:** - -#### 1. Update Plugin Files -- Make your code changes in `plugins//` -- Files: `manager.py`, `config_schema.json`, etc. - -#### 2. Bump Version in `manifest.json` -- **Bump the `version` field** using semantic versioning (MAJOR.MINOR.PATCH) -- Update the `versions` array — add the NEW version FIRST (most recent at top): - ```json - "version": "1.2.3", - - "versions": [ - { - "released": "2026-02-11", - "version": "1.2.3", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2025-10-20", - "version": "1.2.2", - ... - } - ] - ``` - -#### 3. Commit -- The **pre-commit hook** automatically runs `update_registry.py` and stages `plugins.json` -- You do NOT need to manually run `update_registry.py` or manually edit `plugins.json` - -```bash -git add plugins// -git commit -m "fix(plugin-id): description of change" -git push origin main -``` - -**If the pre-commit hook is not installed:** `cp scripts/pre-commit .git/hooks/pre-commit` - ---- - -## Version Numbering Guidelines - -### When to Bump MAJOR (x.0.0) -- Breaking changes to config schema (not backward compatible) -- Removed features or config options -- Complete rewrite or architecture change - -### When to Bump MINOR (1.x.0) -- New features added -- New config options (backward compatible) -- New display modes or functionality - -### When to Bump PATCH (1.2.x) -- Bug fixes -- Performance improvements -- Documentation updates -- Minor tweaks - ---- - -## Common Pitfalls - -### Forgetting to Bump Version -**Problem**: Users won't receive the update — the store compares `manifest.json` version against `plugins.json` `latest_version`. -**Solution**: Always bump `version` in `manifest.json` for every change. - -### Version Mismatch -**Problem**: `version` field doesn't match top entry in `versions` array. -**Solution**: Keep both in sync. - -### Cross-Plugin Module Collisions -**Problem**: The core loads each plugin's top-level `*.py` files by bare name. A -helper module imported from a subpackage or from inside a function/method body (a -*deferred* import) can bind a **different** plugin's same-named module and fail to -load (real case: two plugins both shipping `data_model.py`). -**Solution**: Give deferred/subpackage helper modules plugin-unique names (e.g. -`election_data_model.py`, not `data_model.py`). Relative imports don't work (the -entry point loads with no package context). CI runs -`scripts/check_module_collisions.py` on every PR; run it locally too. - -### Rendering Off-Panel / Not Scaling -**Problem**: Plugin renders past the edge, crashes, or stays tiny on large panels. -**Solution**: Render correctly at all four sizes (64×32, 128×32, 128×64, 256×32). -The core **safety harness** (`test-plugins.yml`) renders every screen at every -size on each PR and fails on overflow/crash. See -`docs/plugin-development/05-adaptive-layout.md`. - ---- - -## Plugin Manifest Required Fields - -Every `plugins//manifest.json` must have: -- `id` — Plugin identifier (must match directory name) -- `name` — Human-readable display name -- `version` — Semver string (e.g., "1.2.3") -- `class_name` — Python class name in manager.py -- `display_modes` — Array of supported display modes - -## Registry Format - -`plugins.json` entries for monorepo plugins use: -- `repo`: `https://github.com/ChuckBuilds/ledmatrix-plugins` -- `plugin_path`: `plugins/` -- `branch`: `main` -- `latest_version`: Synced from manifest by `update_registry.py` - -Third-party plugins keep their own `repo` URL and empty `plugin_path`. - ---- - -## Checklist - -Before pushing a plugin update: - -- [ ] Code changes completed and tested -- [ ] `manifest.json` version bumped -- [ ] New version added to TOP of `versions` array -- [ ] Pre-commit hook installed (`cp scripts/pre-commit .git/hooks/pre-commit`) -- [ ] Committed and pushed - ---- - -## Integration with LEDMatrix Core - -When updating the **LEDMatrix core** (not plugins), you don't need to update the registry. - -But if you update: -- `plugin_system/base_plugin.py` -- `web_interface/templates/v3/partials/plugins.html` -- API endpoints used by plugins - -**Then** you should: -1. Test all installed plugins still work -2. Update minimum LEDMatrix version in affected plugin manifests -3. Document breaking changes +This repo is **plugins + registry only**. Core (`BasePlugin`, harness, +web UI) lives in `ChuckBuilds/LEDMatrix` — don’t apply core-edit checklists here. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..61899d36 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# AGENTS.md — Cursor entry for ledmatrix-plugins + +Read **`CLAUDE.md`** first for the full harness (goal, non-negotiables, local +setup, “working” definition, session-memory rules). Keep this file short. + +## Goal + +Maintain official LEDMatrix plugins + the Plugin Store registry. Core display +runtime is **not** in this repo (`ChuckBuilds/LEDMatrix`). Success = plugins +that version correctly for the store, load without module collisions, and pass +the render safety harness. + +## Always do + +- Bump `plugins//manifest.json` `version` + top `versions[]` entry on any + non-`test/` change under that plugin. +- Never hand-edit `plugins.json` (use pre-commit / `update_registry.py`). +- Fetch in `update()`, draw in `display()`; cache with plugin-id-namespaced keys. +- Unique names for deferred/subpackage modules; run + `python scripts/check_module_collisions.py`. +- Guard optional core imports with `ImportError` fallbacks. +- Before sports “shared” file edits → + `docs/plugin-development/08-shared-sports-code.md` (port across lineage). + +## Before calling a change done + +Harness green (core `check_plugin.py`), collision check OK, version bumped, +no secrets committed. “Looked fine on one emulator size” is not enough. + +## Memory + +Promote repeated corrections and cold-start gotchas into `CLAUDE.md` in-session. +Decay stale/duplicated rules. Don’t leave load-bearing facts only in chat. + +## Pointers + +| Need | Where | +|------|--------| +| Dense harness | `CLAUDE.md` | +| Human guide | `docs/plugin-development/` | +| Contribute / symlink setup | `CONTRIBUTING.md` | +| Submit / verify plugin | `SUBMISSION.md`, `VERIFICATION.md` | diff --git a/CLAUDE.md b/CLAUDE.md index 4f8f7bc8..93e7c45c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,342 +1,227 @@ -# LEDMatrix Plugins Monorepo - -This repo is the **official plugin registry** for [LEDMatrix](https://github.com/ChuckBuilds/LEDMatrix). -Each plugin is a self-contained Python package that the LEDMatrix core loads at -runtime and renders on an RGB LED matrix. The core lives in a separate repo -(`ChuckBuilds/LEDMatrix`); this repo only ships plugin source + the registry the -plugin store reads. - -## Structure -- `plugins//` — Each plugin's source code, manifest, config schema, README, tests -- `plugins.json` — Central registry consumed by the LEDMatrix plugin store (auto-generated; do not hand-edit) -- `update_registry.py` — Syncs `plugins.json` `latest_version` from local plugin manifests -- `scripts/` — `check_module_collisions.py`, `check_team_pickers.py`, `pre-commit` hook, `archive_old_repos.sh` -- `.github/workflows/` — CI: module-collisions, plugin safety harness, registry auto-update -- `schema/` reference and `docs/` — supporting material; canonical `manifest_schema.json` lives in the **core** repo - -There are ~39 plugins in `plugins/`; `plugins.json` also lists third-party plugins hosted in their own repos. - -**Deep-dive human guide:** `docs/plugin-development/` holds the full developer -guide (anatomy, core API, advanced features, styling/skins, adaptive layout, -manifest/schema, testing/CI). This file is the dense LLM-facing summary of it. - -## Anatomy of a Plugin - -A plugin directory contains at minimum a `manifest.json` and an entry-point -Python file (default `manager.py`) with the plugin class. Typical layout: +# LEDMatrix Plugins Monorepo — Agent Harness + +## What this project is + +This repo is the **official plugin registry + plugin source** for +[LEDMatrix](https://github.com/ChuckBuilds/LEDMatrix). It does **not** contain +the display core. The core lives in a separate repo (`ChuckBuilds/LEDMatrix`); +this repo ships: + +- `plugins//` — ~39 self-contained Python plugins the core loads +- `plugins.json` — registry the in-app Plugin Store consumes (**auto-generated; + never hand-edit**) +- tooling/CI that keeps versions, collisions, and render safety honest + +**Goal for agent work here:** ship plugins that install from the store, load +cleanly beside other plugins, fetch safely, and render correctly on real LED +panels — without breaking the version → store update pipeline. + +Human deep-dive: `docs/plugin-development/`. This file is the dense, LLM-facing +harness. Prefer pointing at docs over restating them. + +--- + +## Non-negotiables (project-specific) + +These are not general Python advice — they exist because of how *this* stack works. + +1. **Bump `manifest.json` `version` on every plugin change** (semver). Add the + new entry at the **top** of the `versions` array; keep `version` in sync with + that top entry. The store compares manifest version to `plugins.json` + `latest_version`. Forget the bump → users never get the update; CI fails the PR. + Even README-only edits under `plugins//` (outside `test/`) count. +2. **Never hand-edit `plugins.json`.** Commit with the pre-commit hook + (`cp scripts/pre-commit .git/hooks/pre-commit`) or run + `python update_registry.py`. CI also regenerates on push to `main`. +3. **Fetch in `update()`, draw in `display()`.** Never hit the network from + `display()`. Cache network data via `self.cache_manager`, keys namespaced by + plugin id. +4. **Deferred/subpackage modules need plugin-unique names** (e.g. + `election_data_model.py`, not `data_model.py`). The core loads top-level + `*.py` as bare names, then isolates; late imports can bind another plugin's + module. Relative imports do not work (no package context). Enforce with + `python scripts/check_module_collisions.py`. +5. **Render must survive the safety harness** — no crash, nothing drawn past + the panel edge. Default matrix sizes are eight (see + `docs/plugin-development/07-testing-ci-and-registry.md`); design for the + classic four first (64×32, 128×32, 128×64, 256×32). Harness lives in the + **core** repo: `LEDMatrix/scripts/check_plugin.py`. +6. **Guard optional core imports** (`VegasDisplayMode`, `src.adaptive_layout`, + `src.element_style`, core `BaseOddsManager`, …) with `try/except ImportError` + and a classic fallback — older cores stay loadable. +7. **Shared sports modules are copied, not shared.** Scoreboards ship divergent + copies of `sports.py` / `scroll_display.py` / etc. A fix in one lineage must + be ported to siblings in the **same PR**. See + `docs/plugin-development/08-shared-sports-code.md` before touching those files. +8. **Secrets never in git.** Real tokens live on the Pi / LEDMatrix runtime + (`config_secrets.json`), not in this monorepo. Keep only + `config_secrets.template.json` in git; mark secret schema fields `x-secret`. + OAuth artifacts (`credentials.json`, `token.pickle`) are gitignored at the + repo root. + +Starter template: `plugins/hello-world/`. Best feature references: sports +scoreboards (`hockey-scoreboard`, `football-scoreboard`, …). + +--- + +## Layout (quick) ```text -plugins// - manifest.json # metadata (required) — see fields below - manager.py # entry point; default class location - config_schema.json # JSON Schema Draft-07 for the web-UI config form - requirements.txt # plugin runtime deps (installed by CI before the harness) - README.md # user docs - assets/ # fonts, logos, images - test/ # optional safety-harness fixtures (harness.json, golden/) - test_*.py # optional unit tests +plugins// manifest.json, manager.py (or entry_point), config_schema.json, + requirements.txt, README, assets/, optional test/ +plugins.json store registry (generated) +update_registry.py +scripts/ collision check, team pickers, pre-commit, … +docs/plugin-development/ human guide ``` -The plugin class **inherits from `BasePlugin`** (`src.plugin_system.base_plugin.BasePlugin` -in the core repo) and is constructed as -`__init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager)`. -Key methods (see `plugins/hello-world/manager.py` for a minimal reference): -- `update(self)` — fetch/refresh data (called on `update_interval`); never draw to - `self.display_manager` here. Pre-rendering *offscreen* images into your own cache - does belong here — see "Pre-rendering" below -- `display(self, force_clear=False)` — render via `self.display_manager` then call `update_display()` -- `validate_config(self)` — call `super().validate_config()` then check plugin-specific keys -- `get_info(self)` / `cleanup(self)` — web-UI info and unload teardown -- `self.logger`, `self.config`, `self.display_manager`, `self.cache_manager`, - `self.plugin_manager` (+ `.font_manager`), `self.plugin_id`, `self.enabled`, - `self.global_config` are provided by the base class - -Multi-mode plugins (the scoreboards) use a wider `display(self, display_mode=None, -force_clear=False) -> bool` signature. The core also calls a family of **optional -hooks** if implemented — dynamic duration (`supports_dynamic_duration`, -`get_cycle_duration`, `get_dynamic_duration_cap`/`_floor`, `is_cycle_complete`, -`reset_cycle_state`), live rotation (`get_live_modes`, `has_live_priority`, -`has_live_content`), Vegas (`get_vegas_content`/`_type`/`_display_mode`, -`get_supported_vegas_modes`), and lifecycle (`on_config_change`, `on_enable`, -`on_disable`). See `docs/plugin-development/`. - -`hello-world` is the starter template; new plugins should begin there. - -## Advanced Plugin Features - -These are the optional capabilities that make plugins feature-rich. They're -opt-in: a plugin only participates by reading the relevant config key or -implementing the relevant method. The sports scoreboards (`hockey-scoreboard`, -`football-scoreboard`, `baseball-scoreboard`, …) exercise most of them and are -the best reference implementations. - -### Cache management (`self.cache_manager`) -Every plugin is handed a shared `cache_manager` in its constructor. Use it for -**anything fetched over the network** so restarts and multiple plugins don't -re-hit APIs. Core API surface (seen across plugins): -- `cache_manager.get(key, max_age=)` — return cached value or `None` if missing/stale -- `cache_manager.set(key, value, ttl=)` — store with an optional time-to-live -- `cache_manager.get_cached_data_with_strategy(key, strategy)` / `save_cache(key, data)` — - strategy-driven caching (e.g. `'leaderboard'`) that layers TTL/refresh policy on top of raw get/set; - see `plugins/ledmatrix-leaderboard/data_fetcher.py` -- `cache_manager.delete(key)` / `clear_cache()` — invalidation - -Namespace your keys with the plugin id (e.g. `f"{self.plugin_id}:standings:{league}"`) -so they never collide with another plugin's entries. Fetch in `update()`, never in `display()`. - -### Live priority (`live_priority`) -A per-source boolean (`config[]["live_priority"]`) that tells the -rotation to **prefer live games over scheduled/recent ones**. When enabled and -real live games exist, the manager surfaces only the live content; when no games -are live it falls back to the normal schedule. Wire it up by reading the flag in -`__init__` and filtering the display list in your update/selection logic (see -`plugins/hockey-scoreboard/manager.py`, `nhl_live_priority` and the live-mode -filter around the `_should_show` logic). - -Related live-rotation knobs the sports plugins expose: -- **`favorite_live_boost`** — how many rotation turns a favorite team's live game - gets per turn for other live games (and it's queued first on each refresh). -- **`non_favorite_live` / live-duration overrides** — different display durations - for favorite vs. non-favorite live games. - -### Dynamic duration (`supports_dynamic_duration()` + `dynamic_duration` config) -Lets a plugin tell the core "hold my screen for a computed time" instead of a -fixed `display_duration`. The plugin implements -`supports_dynamic_duration(self, mode_type=None) -> bool` and reads a -`dynamic_duration` config object (`enabled`, `max_duration_seconds`, per-mode -settings). Typical use: size the on-screen time to the width of scrolling content, -or extend live games. See `plugins/football-scoreboard/DYNAMIC_DURATION.md` and -the `supports_dynamic_duration` implementations in the sports managers. - -### Pre-rendering (expensive images belong in `update()`) -"Never draw in `update()`" means never touch `self.display_manager` there — not -that `update()` may not build images. Building a PIL image into the plugin's own -cache is expensive work, and expensive work is exactly what `update()` is for: -it runs on the update worker, while `display()` runs on the render thread, where -a stall shows up as a frozen panel or a stuttering marquee. So a plugin whose -frame costs real time should render offscreen in `update()` and have `display()` -do nothing but paste the result and draw the parts that must be live (a clock, a -countdown). Precedent: `plugins/f1-scoreboard/manager.py` -(`_prepare_scroll_content`, 12.46s of scroll images), `plugins/ledmatrix-elections` -(`_build_scroll_image`), `plugins/geochron` (`_render_for_size`). - -Two things make this safe: -- **Key the cache on `(width, height)`.** Vegas captures plugins at a narrower - width than the panel (`vegas_width_pct`), so the same plugin is asked to render - at two sizes and a single-entry cache thrashes between them. Re-render every - cached size in `update()`. -- **Keep a lazy path in `display()`.** A size that has never been rendered has to - be built on demand; that's a one-off, not the steady state. - -### High-FPS / smooth scrolling -Scrolling plugins render far faster than the default loop for smooth motion. -Two mechanisms: -- **Global target FPS** — read `global_config['target_fps']` (fallback - `scroll_target_fps`, default ~100) and push it into the scroll helper: - `self.scroll_helper.set_target_fps(target_fps)`, with a clamp fallback - (`max(30.0, min(200.0, target_fps))`) for older cores. See - `plugins/odds-ticker/manager.py`, `plugins/news/manager.py`, - `plugins/ledmatrix-leaderboard/manager.py`. -- **Per-frame delay** — the `scroll_delay` config key (seconds/frame; `0.01` ≈ 100 FPS) - controls smoothness on the scoreboards. -- **Per-plugin high-performance flag** — e.g. `high_performance_transitions` in - `plugins/christmas-countdown/config_schema.json` toggles 120 FPS transitions vs. 30 FPS. - -### Vegas mode (continuous scroll integration) -"Vegas" is the core's continuous marquee that stitches multiple plugins into one -endlessly-scrolling strip. A plugin opts in by implementing: -- `get_vegas_content(self)` — return the PIL image(s) to splice into the strip (or `None`) -- `get_vegas_content_type(self)` — `'single'` or `'multi'` (multiple scrollable items, e.g. games) -- `get_vegas_display_mode(self)` — return a `VegasDisplayMode`, honoring the - `vegas_mode` config override - -Import the enum defensively, since older cores don't ship it: -```python -try: - from src.plugin_system.base_plugin import BasePlugin, VegasDisplayMode -except ImportError: - VegasDisplayMode = None -``` -The `vegas_mode` config key (mark it `x-advanced`) is an enum: -- `scroll` — items scroll individually through the stream (default) -- `fixed` — the whole display scrolls by as one block -- `static` — the marquee pauses while the plugin shows for its duration - -See `plugins/hockey-scoreboard/manager.py` (Vegas section) and -`plugins/nfl-draft/config_schema.json` / `plugins/olympics/config_schema.json` -for the config declaration. - -### Adaptive layout (`layout_mode`) -Every plugin must render on all four sizes (64×32, 128×32, 128×64, 256×32). Most -adapt with plain width/height-tier branching off `self.display_manager.width/height` -(e.g. `ledmatrix-flights/renderer.py`, `masters-tournament`). Two plugins -(`football-scoreboard`, `ledmatrix-music`) additionally opt into the core's -**adaptive engine** via a `layout_mode` config enum (`["classic","adaptive"]`, -default `classic`, `x-advanced`) — note the key is `layout_mode`, not -`layout_engine`. `adaptive` (beta) scales fonts/logos/regions to the panel using -`src.adaptive_layout` (guarded import; falls back to classic on older cores) while -still honoring user font overrides and `customization.layout` x/y offsets. See -`plugins/football-scoreboard/game_renderer.py` and -`docs/plugin-development/05-adaptive-layout.md`. - -## Module Naming — Avoid Cross-Plugin Collisions - -The core loads every plugin's top-level `*.py` files as **bare-name** modules on -`sys.path` (e.g. `import data_model`), then namespace-isolates them *after* the -entry point finishes loading. Two plugins **may** ship identically-named -top-level modules (the sports plugins all share `sports.py`, `scroll_display.py`, -…) — but only if every intra-plugin import runs **while the entry point is -loading**. - -It breaks for **deferred imports** — a `from data_model import X` that runs -*after* isolation: -- inside a **subpackage** `__init__`/module that's imported lazily during - instantiation (e.g. `providers/__init__.py`), or -- inside a **function/method body** that runs at update/display time. - -By then the bare name has been popped from `sys.modules`, so the import -re-resolves via `sys.path` and can bind a **different plugin's** identically-named -module — the plugin fails to load. (Real case: `ledmatrix-elections` and -`ledmatrix-flights` both shipped `data_model.py`; elections' `providers/` -subpackage bound flights' `data_model` and failed.) - -**Rule:** if a module is imported from a subpackage or a deferred (function-scoped) -position, give it a **plugin-unique name** — prefix with the plugin domain, e.g. -`election_data_model.py`, not `data_model.py`. Relative imports are **not** an -option: the loader loads the entry point via `spec_from_file_location` with no -package context, so `from .data_model import X` raises "no known parent package." - -**Enforcement:** `scripts/check_module_collisions.py` fails CI when a plugin's -deferred import targets a sibling top-level module whose name is also shipped by -another plugin. It runs on every PR via `.github/workflows/module-collisions.yml`. -Run it locally with `python scripts/check_module_collisions.py`. - -## Plugin Version Workflow - -**IMPORTANT:** When modifying any plugin, you MUST bump its version. This is how users receive updates — the LEDMatrix plugin store compares `manifest.json` version against `plugins.json` latest_version. - -### Steps for every plugin change: -1. Make your code changes in `plugins//` -2. Bump `version` in `plugins//manifest.json` (semver: major.minor.patch) -3. Commit — the pre-commit hook automatically runs `update_registry.py` and stages `plugins.json` - -> **Note:** The pre-commit hook only triggers when a `plugins/*/manifest.json` is staged. If it's not installed, run `cp scripts/pre-commit .git/hooks/pre-commit` to set it up. - -### Version bump guidelines: -- **Patch** (1.0.0 → 1.0.1): Bug fixes, minor text changes -- **Minor** (1.0.0 → 1.1.0): New features, config schema additions -- **Major** (1.0.0 → 2.0.0): Breaking config changes, major rewrites - -### If you forget to bump the version: -Users will NOT receive the update. The store uses version comparison, not git commits. CI (`test-plugins.yml`) **fails a PR** whose plugin code changed without a version bump. - -## Plugin Manifest Fields -Every `plugins//manifest.json` is validated against the core repo's -`schema/manifest_schema.json`. Core required fields: -- `id` — Plugin identifier (must match directory name) -- `name` — Human-readable display name -- `version` — Semver string (e.g., "1.2.3") -- `class_name` — Python class name in the entry point -- `display_modes` — Array of supported display mode names - -Commonly present optional fields: -- `entry_point` — Python file with the plugin class (default `manager.py`) -- `author`, `description`, `category`, `tags` — store metadata -- `config_schema` — path to the config schema file (usually `config_schema.json`) -- `versions` — changelog array of `{version, released, ledmatrix_min}` entries -- `compatible_versions` / `ledmatrix_min` — core-version compatibility constraints -- `verified`, `stars`, `downloads`, `screenshot`, `last_updated` — store display fields - -## Config Schema Conventions (`config_schema.json`) -Config schemas are **JSON Schema Draft-07** (all 39 plugins) and drive the -auto-generated web-UI config form. Beyond standard JSON Schema, the UI honors -custom `x-` extensions (validators ignore unknown `x-` keys): -- **`x-advanced: true`** — hide a property behind the **Advanced Settings** - disclosure; use for fine-tuning knobs. By far the most used. -- **`x-propertyOrder`** (array) — explicit property render order. -- **`x-widget`** (string) — custom editor: `color-picker`, `checkbox-group`, - `file-upload`, `array-table`, `radio`, `time-picker`, `plugin-file-manager`, … - (companions: `x-widget-config`, `x-upload-config`, `x-columns`, `x-options`). -- **`x-collapsed`** — section collapsed by default. **`x-secret`/`x-sensitive`** — - mask value. **`x-placeholder`**, **`x-display`** (`"hidden"`). - -**Styling / "skins" — two mechanisms.** (1) The manual `customization` block: -per-element objects with `font`/`font_size`/`text_color` (~17 plugins; e.g. -`plugins/clock-simple/config_schema.json`), self-contained and core-agnostic. -(2) `x-style-elements`: a compact shorthand on `customization` that a newer core -expands into a full font/size/color/offset UI via `src.element_style` -(guarded import + classic fallback). Only `plugins/of-the-day` uses it — the -reference. See `docs/plugin-development/04-styling-and-skins.md`. - -Mirror the property `default`s in the plugin code's `config.get(key, default)` -calls so behavior matches the schema even when a key is absent. - -## Registry Format -`plugins.json` is generated by `update_registry.py` — **do not hand-edit it**; it -has top-level `version`, `last_updated`, and a `plugins` array. Entries for -monorepo plugins use: -- `repo`: `https://github.com/ChuckBuilds/ledmatrix-plugins` -- `plugin_path`: `plugins/` -- `branch`: `main` -- `latest_version`: Synced from manifest by `update_registry.py` - -Third-party plugins keep their own `repo` URL and empty `plugin_path`. - -## Scripts -- `python update_registry.py` — Update plugins.json from manifests -- `python update_registry.py --dry-run` — Preview without writing -- `python scripts/check_module_collisions.py` — Cross-plugin module-collision check -- `python scripts/check_team_pickers.py` — Compare `favorite_teams` pickers against ESPN (`--apply` regenerates the enums; label differences only warn) -- `scripts/archive_old_repos.sh` — Archive old individual repos (one-time, use `--apply`) - -## Git Hooks -- `scripts/pre-commit` — Auto-syncs `plugins.json` when manifest versions change -- Install: `cp scripts/pre-commit .git/hooks/pre-commit` - -## CI Workflows (`.github/workflows/`) -- **`test-plugins.yml`** (Plugin Safety) — on PRs touching `plugins/**`: for each - *changed* plugin (non-test code only) it enforces the version bump, validates the - manifest against the schema, installs the plugin's `requirements.txt`, and runs - the core safety harness. Test-only changes (`plugins//test/**`) don't trigger the gate. -- **`module-collisions.yml`** (Module Collisions) — on PRs touching `plugins/**`: - runs `check_module_collisions.py` across **all** plugins. -- **`update-registry.yml`** (Update Plugin Registry) — on push to `main` touching a - manifest or `update_registry.py`: regenerates `plugins.json` and auto-commits it. - -## Plugin Safety Harness (cross-size / cross-screen) - -Each plugin can expose multiple screens and must render on every supported matrix -size (64×32, 128×32, 128×64, 256×32). The harness lives in the **core** repo -(`LEDMatrix/scripts/check_plugin.py`) and renders every screen at every size, -failing on crashes, content drawn past the panel edge, or visual drift vs -committed golden images. - -**Before opening a PR that changes a plugin:** +Plugin class: subclass `BasePlugin` from the core +(`src.plugin_system.base_plugin.BasePlugin`). Constructor args: +`plugin_id, config, display_manager, cache_manager, plugin_manager`. + +Required manifest fields: `id` (matches directory), `name`, `version`, +`class_name`, `display_modes`. Full field list / schema conventions → +`docs/plugin-development/06-manifest-and-config-schema.md`. + +Config schemas are JSON Schema Draft-07 with UI `x-*` extensions +(`x-advanced`, `x-widget`, `x-secret`, …). Mirror schema `default`s in +`config.get(key, default)`. Details → docs topic 4 and 6. + +Advanced opt-ins (cache, live priority, dynamic duration, Vegas, adaptive +`layout_mode`, high-FPS scroll) → `docs/plugin-development/03-advanced-features.md` +and topic 5. Do not reinvent the API catalog here. + +--- + +## Version bumps + +| Bump | When | +|------|------| +| **PATCH** | Bug fix, perf, docs-only in plugin tree | +| **MINOR** | New feature or backward-compatible config keys / modes | +| **MAJOR** | Breaking schema/config, removed options, rewrite | + +After code change: bump manifest → commit (hook syncs registry) → PR. CI +(`.github/workflows/test-plugins.yml`) enforces bump + harness + schema; +`module-collisions.yml` runs across all plugins. + +--- + +## Local setup (bus-factor) + +Cold-start facts that are easy to rediscover the hard way: + +- **This tree must be a git clone to contribute.** A zip extract under + `Downloads/` has no `.git`, so hooks, PRs, and version-bump CI context are + unavailable. Prefer: + `git clone https://github.com/ChuckBuilds/ledmatrix-plugins.git` +- **You need a sibling LEDMatrix core checkout** for the harness, emulator, and + `BasePlugin` imports: + ```bash + git clone https://github.com/ChuckBuilds/LEDMatrix.git + git clone https://github.com/ChuckBuilds/ledmatrix-plugins.git + cd LEDMatrix + ln -s ../ledmatrix-plugins/plugins/ plugin-repos/ + # or: scripts/dev/dev_plugin_setup.sh + python3 scripts/dev_server.py # http://localhost:5001 + EMULATOR=true python3 run.py + ``` +- **Harness before PR:** + ```bash + # from LEDMatrix core checkout + python scripts/check_plugin.py --plugin \ + --plugin-dir /path/to/ledmatrix-plugins/plugins --out-dir /tmp/preview + ``` +- **Install the pre-commit hook** in the plugins repo: + `cp scripts/pre-commit .git/hooks/pre-commit` +- **Secrets:** core/runtime `config_secrets.json` (not this repo); plugin-local + `plugins/**/config_secrets.json` is gitignored. Never commit real tokens. +- **Registry:** `update_registry.py` only updates `latest_version` from local + manifests for monorepo plugins; third-party entries keep their own `repo` URL + and empty `plugin_path`. + +More: `CONTRIBUTING.md`, `SUBMISSION.md`, `VERIFICATION.md`. + +--- + +## What “working” means + +A change is **correct** only if all of the following hold for touched plugins: + +| Signal | How we know it’s broken | Automatic check | +|--------|-------------------------|-----------------| +| Store can ship the update | Version not bumped / mismatches `versions[0]` / `plugins.json` stale | CI version gate + pre-commit `update_registry.py` | +| Plugin loads beside others | Deferred import binds another plugin’s module | `scripts/check_module_collisions.py` + CI | +| Manifest valid | Missing required fields / schema drift | CI vs core `schema/manifest_schema.json` | +| Renders on panels | Crash, draw past edge, or golden drift | Core `check_plugin.py` + CI harness | +| Config UI ↔ runtime | Schema default ≠ `config.get` default; README tables lie | Manual + PR checklist; prefer matching schema | +| No secret leak | Key/token committed | `.gitignore` + PR template / VERIFICATION | + +Optional but strong: commit `plugins//test/harness.json` + golden PNGs so +visual drift fails CI instead of showing up on a Pi. + +**Do not treat as “works”:** “looks fine on one size in the emulator once.” + +--- + +## Session memory (compounding) + +Harness knowledge dies when it only lives in a chat transcript or a one-off fix. + +**Capture → promote → decay** + +1. **Capture** — When Jean corrects the same fact twice, or a cold-start + rediscovery costs real time (setup path, sports lineage, harness sizes, + secret location), write it into **this file** under the right section in the + same session. Do not leave it only in chat. +2. **Promote** — Standing decisions belong here as imperative rules (the + Non-negotiables list). Long tutorials belong in `docs/plugin-development/`; + link them. Duplicate the same rule in `.cursorrules` / `AGENTS.md` only as a + short pointer — one source of truth. +3. **Decay** — Remove or fix instructions that contradict the docs (e.g. stale + “four sizes only” when the harness matrix grew), leftover **core-repo** edit + checklists that don’t apply to this plugins monorepo, and API laundry lists + that a model already knows once pointed at a reference plugin. +4. **Do not accumulate** — No changelog of every session. Prefer fewer, sharper + rules. If a section grows past “skim in 30 seconds,” move detail to docs and + keep the rule + link. + +**Resurface each session:** read `AGENTS.md` (entry) + this file’s +Non-negotiables before editing a plugin. For sports shared-file edits, open +topic 08 first. + +--- + +## Standing decisions (so we don’t re-argue) + +- Prefer editing an existing plugin’s patterns over inventing new architecture. +- New plugins start from `hello-world`, not by copying a full scoreboard. +- Mark fine-tuning config keys `x-advanced: true`. +- `layout_mode` (not `layout_engine`) for adaptive opt-in; default `classic`. +- When both a fix and an exploratory rewrite are possible, ship the smallest + fix that restores harness green + correct store versioning. +- Commit only when Jean asks; don’t push unless asked. +- Don’t edit `plugins.json` by hand to “help.” + +--- + +## Scripts cheat sheet + ```bash -# from a LEDMatrix (core) checkout, with the monorepo plugins on the path: -python scripts/check_plugin.py --plugin \ - --plugin-dir /path/to/ledmatrix-plugins/plugins --out-dir /tmp/preview +python update_registry.py # sync plugins.json from manifests +python update_registry.py --dry-run +python scripts/check_module_collisions.py +python scripts/check_team_pickers.py # --apply regenerates ESPN enums ``` -Eyeball the PNGs in `/tmp/preview`, then fix any FAIL (overflow/crash) before pushing. -**Golden images (optional, per plugin):** commit reference PNGs so visual drift is -caught automatically: -```text -plugins//test/harness.json # deterministic config / mock data / frozen time -plugins//test/golden//.png -``` -Regenerate with `check_plugin.py --update-golden` and review the diff. See -`clock-simple/test/` for a worked example and `LEDMatrix/docs/plugin-safety-harness.md` -for the full reference. - -**CI:** `.github/workflows/test-plugins.yml` runs the harness against every -*changed* plugin on each PR (installs that plugin's `requirements.txt` first), -validates its manifest against `schema/manifest_schema.json`, and enforces the -version bump. - -## Quick Reference — Making a Plugin Change -1. Edit code in `plugins//` (keep deferred/subpackage module names plugin-unique). -2. Update `config_schema.json` if config changed (mark fine-tuning keys `x-advanced`). -3. **Bump `version`** in `manifest.json`. -4. Run `python scripts/check_module_collisions.py` and, from a core checkout, the safety harness. -5. Commit (pre-commit hook syncs `plugins.json`) and open a PR — CI enforces the version bump, manifest schema, harness, and collisions. +CI: `test-plugins.yml` (bump + schema + harness), `module-collisions.yml`, +`update-registry.yml` (push to `main`). + +--- + +## Out of scope here + +- Changing LEDMatrix **core** APIs, web UI templates, or `BasePlugin` — that’s + the other repo. If a plugin needs a newer core API, bump `ledmatrix_min` / + `compatible_versions` in the manifest and document it; don’t pretend core + files live in this tree. diff --git a/plugins.json b/plugins.json index f74ba09a..bdd47c28 100644 --- a/plugins.json +++ b/plugins.json @@ -124,7 +124,7 @@ "last_updated": "2026-08-13", "verified": true, "screenshot": "", - "latest_version": "1.2.0" + "latest_version": "1.2.1" }, { "id": "christmas-countdown", @@ -385,7 +385,7 @@ "last_updated": "2026-08-05", "verified": true, "screenshot": "", - "latest_version": "1.2.0" + "latest_version": "1.2.1" }, { "id": "leaderboard", @@ -510,7 +510,7 @@ "last_updated": "2026-07-31", "verified": true, "screenshot": "", - "latest_version": "1.1.0" + "latest_version": "1.1.1" }, { "id": "music", @@ -581,7 +581,7 @@ "last_updated": "2026-07-31", "verified": true, "screenshot": "", - "latest_version": "1.2.4", + "latest_version": "1.2.5", "icon": "fa-circle-dot" }, { @@ -928,7 +928,7 @@ "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", "branch": "main", "plugin_path": "plugins/ledmatrix-weather", - "latest_version": "2.6.4", + "latest_version": "2.6.5", "stars": 0, "downloads": 0, "last_updated": "2026-08-05", @@ -1168,7 +1168,7 @@ "last_updated": "2026-07-29", "verified": true, "screenshot": "", - "latest_version": "1.3.3", + "latest_version": "1.3.4", "icon": "fa-hourglass-half" } ] diff --git a/plugins/birdnet-go/config_schema.json b/plugins/birdnet-go/config_schema.json index 015060a5..7cbe89c2 100644 --- a/plugins/birdnet-go/config_schema.json +++ b/plugins/birdnet-go/config_schema.json @@ -44,7 +44,8 @@ "password": { "type": "string", "default": "", - "description": "MQTT broker password (optional)" + "description": "MQTT broker password (optional)", + "x-secret": true }, "client_id": { "type": "string", diff --git a/plugins/birdnet-go/manifest.json b/plugins/birdnet-go/manifest.json index 21b8c8ec..68940a64 100644 --- a/plugins/birdnet-go/manifest.json +++ b/plugins/birdnet-go/manifest.json @@ -1,9 +1,9 @@ { "id": "birdnet-go", "name": "BirdNET-Go", - "version": "1.2.0", + "version": "1.2.1", "author": "ChuckBuilds", - "description": "Show what BirdNET-Go is hearing. One screen cycles a different species each turn — name, confidence, how many times it has been heard today, and a photo — and a second shows today's stats: species count, total detections and the most-heard species. Polls the BirdNET-Go REST API, with optional MQTT for instant pop-ups.", + "description": "Show what BirdNET-Go is hearing. One screen cycles a different species each turn \u2014 name, confidence, how many times it has been heard today, and a photo \u2014 and a second shows today's stats: species count, total detections and the most-heard species. Polls the BirdNET-Go REST API, with optional MQTT for instant pop-ups.", "category": "integration", "tags": [ "birdnet", @@ -21,6 +21,12 @@ "entry_point": "manager.py", "class_name": "BirdNetGoPlugin", "versions": [ + { + "released": "2026-08-15", + "version": "1.2.1", + "notes": "Mark MQTT broker password as x-secret so the web UI masks it.", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-08-05", "version": "1.2.0", diff --git a/plugins/calendar/.gitignore b/plugins/calendar/.gitignore index 979acecd..ad1f83ba 100644 --- a/plugins/calendar/.gitignore +++ b/plugins/calendar/.gitignore @@ -144,3 +144,8 @@ Thumbs.db *.log config.json *.pem + +# Google OAuth artifacts (also covered by root .gitignore) +credentials.json +token.pickle +*.pickle diff --git a/plugins/calendar/manifest.json b/plugins/calendar/manifest.json index 59f0fdf2..fd60af02 100644 --- a/plugins/calendar/manifest.json +++ b/plugins/calendar/manifest.json @@ -1,7 +1,7 @@ { "id": "calendar", "name": "Google Calendar", - "version": "1.2.0", + "version": "1.2.1", "author": "ChuckBuilds", "description": "Display upcoming events from Google Calendar with date, time, and event details. Shows next 1-3 events with automatic rotation and timezone support.", "category": "productivity", @@ -35,6 +35,12 @@ } ], "versions": [ + { + "released": "2026-08-15", + "version": "1.2.1", + "notes": "Ignore credentials.json and token.pickle in the plugin .gitignore so local Google OAuth artifacts cannot be committed by accident.", + "ledmatrix_min_version": "2.0.0" + }, { "version": "1.2.0", "released": "2026-08-13", diff --git a/plugins/ledmatrix-weather/config_schema.json b/plugins/ledmatrix-weather/config_schema.json index 8ebdab0b..c56e37a2 100644 --- a/plugins/ledmatrix-weather/config_schema.json +++ b/plugins/ledmatrix-weather/config_schema.json @@ -26,6 +26,7 @@ "api_key": { "x-advanced": true, "x-display": "hidden", + "x-secret": true, "type": "string", "description": "Deprecated — no longer used. Kept for backward compatibility with existing configs.", "title": "API Key (deprecated)" diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index a9ae33a9..0284ec87 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-weather", "name": "Weather Display", - "version": "2.6.4", + "version": "2.6.5", "author": "ChuckBuilds", "class_name": "WeatherPlugin", "update_interval": 60, @@ -26,6 +26,12 @@ "radar" ], "versions": [ + { + "version": "2.6.5", + "released": "2026-08-21", + "ledmatrix_min_version": "2.0.0", + "notes": "Mark the deprecated api_key config field x-secret so any leftover value is masked in the web UI. (Originally written against 2.6.4; that number was taken by the radar viewport fix on main, so this lands as 2.6.5 rather than overwriting its changelog entry.)" + }, { "version": "2.6.4", "released": "2026-08-19", @@ -53,7 +59,7 @@ { "released": "2026-07-19", "version": "2.6.0", - "notes": "Radar overhaul. (1) Real map tiles: the radar now draws over an OpenStreetMap basemap (self-hosted tile server supported via radar_tile_server, public mirrors as fallback; carto/carto_dark/esri styles too), with the classic WeatherStar vector map kept as a selectable style and as automatic fallback when tiles are unavailable — radar is no longer blank outside the US. (2) Accuracy fix: basemap and radar are now rendered through one shared Web-Mercator viewport and the radar is a mosaic of every tile in view, so precipitation finally lines up with the map on all panel sizes (previously the radar was composited at a different zoom than the map — up to 4x off on 64x32 — and cut off near tile edges). (3) Fresher data: optional RainViewer nowcast frames (~30 min of predicted radar, labeled FCST +Nm with yellow progress dots), index polled every 3 min by default with tiles only downloaded for new frames, and frames/tiles cached to disk so restarts rebuild the animation without refetching. (4) Easier config: new radar_range_miles (distance to panel edge) replaces the abstract radar_zoom (still honored for existing configs), plus map style/brightness, frame timing, and past-frame-count settings. (5) Smoother playback: time-based frame stepping with a hold on the newest frame, and optional dynamic duration to let the rotation wait for a full loop.", + "notes": "Radar overhaul. (1) Real map tiles: the radar now draws over an OpenStreetMap basemap (self-hosted tile server supported via radar_tile_server, public mirrors as fallback; carto/carto_dark/esri styles too), with the classic WeatherStar vector map kept as a selectable style and as automatic fallback when tiles are unavailable \u2014 radar is no longer blank outside the US. (2) Accuracy fix: basemap and radar are now rendered through one shared Web-Mercator viewport and the radar is a mosaic of every tile in view, so precipitation finally lines up with the map on all panel sizes (previously the radar was composited at a different zoom than the map \u2014 up to 4x off on 64x32 \u2014 and cut off near tile edges). (3) Fresher data: optional RainViewer nowcast frames (~30 min of predicted radar, labeled FCST +Nm with yellow progress dots), index polled every 3 min by default with tiles only downloaded for new frames, and frames/tiles cached to disk so restarts rebuild the animation without refetching. (4) Easier config: new radar_range_miles (distance to panel edge) replaces the abstract radar_zoom (still honored for existing configs), plus map style/brightness, frame timing, and past-frame-count settings. (5) Smoother playback: time-based frame stepping with a hold on the newest frame, and optional dynamic duration to let the rotation wait for a full loop.", "ledmatrix_min": "2.0.0" }, { @@ -77,7 +83,7 @@ { "released": "2026-06-10", "version": "2.5.0", - "note": "Geocode the configured location once and cache the coordinates permanently across update cycles and restarts, instead of re-resolving every refresh. Cities don't move, so the geocoding API is now only ever called on a cache miss — eliminating the per-refresh geocoding timeouts that previously aborted the whole weather update, blanked the widget, and triggered up-to-an-hour error backoff. Add optional location_latitude/location_longitude config fields to skip geocoding entirely.", + "note": "Geocode the configured location once and cache the coordinates permanently across update cycles and restarts, instead of re-resolving every refresh. Cities don't move, so the geocoding API is now only ever called on a cache miss \u2014 eliminating the per-refresh geocoding timeouts that previously aborted the whole weather update, blanked the widget, and triggered up-to-an-hour error backoff. Add optional location_latitude/location_longitude config fields to skip geocoding entirely.", "ledmatrix_min": "2.0.0" }, { diff --git a/plugins/mqtt-notifications/config_schema.json b/plugins/mqtt-notifications/config_schema.json index 84358d34..cd6c8830 100644 --- a/plugins/mqtt-notifications/config_schema.json +++ b/plugins/mqtt-notifications/config_schema.json @@ -47,7 +47,8 @@ "password": { "type": "string", "default": "", - "description": "MQTT broker password (optional)" + "description": "MQTT broker password (optional)", + "x-secret": true }, "client_id": { "type": "string", diff --git a/plugins/mqtt-notifications/manifest.json b/plugins/mqtt-notifications/manifest.json index 071e0f74..21a06b24 100644 --- a/plugins/mqtt-notifications/manifest.json +++ b/plugins/mqtt-notifications/manifest.json @@ -1,7 +1,7 @@ { "id": "mqtt-notifications", "name": "MQTT Notifications", - "version": "1.1.0", + "version": "1.1.1", "author": "ChuckBuilds", "description": "Display text or images from HomeAssistant via MQTT. Supports dynamic MQTT topics with wildcard support for flexible notification handling that interrupts the normal display rotation.", "category": "integration", @@ -18,6 +18,12 @@ "entry_point": "manager.py", "class_name": "MQTTNotificationsPlugin", "versions": [ + { + "version": "1.1.1", + "released": "2026-08-15", + "notes": "Mark MQTT broker password as x-secret so the web UI masks it.", + "ledmatrix_min": "2.0.0" + }, { "version": "1.1.0", "released": "2026-07-31", diff --git a/plugins/on-air/config_schema.json b/plugins/on-air/config_schema.json index c5d5fa82..4b63b801 100644 --- a/plugins/on-air/config_schema.json +++ b/plugins/on-air/config_schema.json @@ -95,6 +95,7 @@ "default": "", "title": "Password", "description": "MQTT broker password. Leave blank if your broker does not require authentication.", + "x-secret": true, "x-sensitive": true }, "command_topic": { diff --git a/plugins/on-air/manifest.json b/plugins/on-air/manifest.json index 30f06d82..f37137be 100644 --- a/plugins/on-air/manifest.json +++ b/plugins/on-air/manifest.json @@ -1,9 +1,9 @@ { "id": "on-air", "name": "On Air Light", - "version": "1.2.4", + "version": "1.2.5", "author": "ChuckBuilds", - "description": "Retro broadcast ON AIR tally light. Activate remotely via MQTT or Home Assistant to signal you're on a call, recording, or live — stays on until you turn it off.", + "description": "Retro broadcast ON AIR tally light. Activate remotely via MQTT or Home Assistant to signal you're on a call, recording, or live \u2014 stays on until you turn it off.", "entry_point": "manager.py", "class_name": "OnAirPlugin", "category": "utility", @@ -22,6 +22,12 @@ ">=2.0.0" ], "versions": [ + { + "version": "1.2.5", + "released": "2026-08-15", + "notes": "Also mark mqtt_password as x-secret (in addition to x-sensitive) so the web UI masks it consistently with other plugins.", + "ledmatrix_min": "2.0.0" + }, { "version": "1.2.4", "released": "2026-07-31", diff --git a/plugins/pomodoro-timer/config_schema.json b/plugins/pomodoro-timer/config_schema.json index 1e29d497..b872e647 100644 --- a/plugins/pomodoro-timer/config_schema.json +++ b/plugins/pomodoro-timer/config_schema.json @@ -139,6 +139,7 @@ "default": "", "title": "Password", "description": "MQTT broker password. Leave blank if your broker does not require authentication.", + "x-secret": true, "x-sensitive": true }, "command_topic": { diff --git a/plugins/pomodoro-timer/manifest.json b/plugins/pomodoro-timer/manifest.json index 08b1b73f..1a58c1ec 100644 --- a/plugins/pomodoro-timer/manifest.json +++ b/plugins/pomodoro-timer/manifest.json @@ -1,9 +1,9 @@ { "id": "pomodoro-timer", "name": "Pomodoro Timer", - "version": "1.3.3", + "version": "1.3.4", "author": "ChuckBuilds", - "description": "A configurable Pomodoro focus/break timer for your matrix. Set the work and break lengths, then start, pause, skip, or reset it over MQTT — with Home Assistant auto-discovery so the whole timer shows up as a device with no YAML.", + "description": "A configurable Pomodoro focus/break timer for your matrix. Set the work and break lengths, then start, pause, skip, or reset it over MQTT \u2014 with Home Assistant auto-discovery so the whole timer shows up as a device with no YAML.", "entry_point": "manager.py", "class_name": "PomodoroTimerPlugin", "category": "productivity", @@ -22,10 +22,16 @@ ">=2.0.0" ], "versions": [ + { + "released": "2026-08-15", + "version": "1.3.4", + "notes": "Also mark mqtt_password as x-secret (in addition to x-sensitive) so the web UI masks it consistently with other plugins.", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-07-29", "version": "1.3.3", - "notes": "Review fixes. A label sent with START is now applied when the command resumes a paused timer, which was the one remaining branch that silently dropped it. Labels from a command payload are bounded to 32 characters and coerced to text, matching the task label — an unbounded string was paying for itself in the renderer's trim-to-fit loop on every frame, and a non-string reached the draw call untouched. Renaming only the state topic now clears the retained payload on the one being abandoned; it is settable independently of the command topic, so the old topic would otherwise sit on the broker forever showing a timer that no longer exists.", + "notes": "Review fixes. A label sent with START is now applied when the command resumes a paused timer, which was the one remaining branch that silently dropped it. Labels from a command payload are bounded to 32 characters and coerced to text, matching the task label \u2014 an unbounded string was paying for itself in the renderer's trim-to-fit loop on every frame, and a non-string reached the draw call untouched. Renaming only the state topic now clears the retained payload on the one being abandoned; it is settable independently of the command topic, so the old topic would otherwise sit on the broker forever showing a timer that no longer exists.", "ledmatrix_min_version": "2.0.0" }, { @@ -37,7 +43,7 @@ { "released": "2026-07-28", "version": "1.3.1", - "notes": "Crispness and bounds. The phase label is now rendered through a thresholded mask instead of PIL's antialiased text, so a frame contains only the palette colours rather than a grey halo around every glyph — 26-36 distinct colours per frame drops to 6, all intentional. Fixes content drawn past the bottom edge on panels under about 16px tall: the countdown box had a hard 6px floor that pushed the digits off-panel once the label had taken its row, so the label is now dropped instead when both cannot fit. Swept 256 panel shapes from 32x8 to 384x128 with no overflow, no crashes, and no blank frames.", + "notes": "Crispness and bounds. The phase label is now rendered through a thresholded mask instead of PIL's antialiased text, so a frame contains only the palette colours rather than a grey halo around every glyph \u2014 26-36 distinct colours per frame drops to 6, all intentional. Fixes content drawn past the bottom edge on panels under about 16px tall: the countdown box had a hard 6px floor that pushed the digits off-panel once the label had taken its row, so the label is now dropped instead when both cannot fit. Swept 256 panel shapes from 32x8 to 384x128 with no overflow, no crashes, and no blank frames.", "ledmatrix_min_version": "2.0.0" }, { From c3de6e90b589d1f4cd2c0c593b0c8feb6ddeca9d Mon Sep 17 00:00:00 2001 From: Jean Mosquea Date: Sat, 15 Aug 2026 23:01:17 -0400 Subject: [PATCH 2/3] Fix Codacy doc refs and align harness size guidance. Use resolvable markdown links to contributor docs, and clarify that plugins design for the classic four panel sizes while CI may exercise a wider harness matrix. Co-authored-by: Cursor --- AGENTS.md | 8 ++++---- CLAUDE.md | 5 +++-- SUBMISSION.md | 6 ++++-- VERIFICATION.md | 5 +++-- docs/plugin-development/05-adaptive-layout.md | 7 ++++--- docs/plugin-development/README.md | 3 ++- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 61899d36..88e97160 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Decay stale/duplicated rules. Don’t leave load-bearing facts only in chat. | Need | Where | |------|--------| -| Dense harness | `CLAUDE.md` | -| Human guide | `docs/plugin-development/` | -| Contribute / symlink setup | `CONTRIBUTING.md` | -| Submit / verify plugin | `SUBMISSION.md`, `VERIFICATION.md` | +| Dense harness | [CLAUDE.md](./CLAUDE.md) | +| Human guide | [docs/plugin-development/](./docs/plugin-development/) | +| Contribute / symlink setup | [CONTRIBUTING.md](./CONTRIBUTING.md) | +| Submit / verify plugin | [SUBMISSION.md](./SUBMISSION.md), [VERIFICATION.md](./VERIFICATION.md) | diff --git a/CLAUDE.md b/CLAUDE.md index 93e7c45c..dd7937d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,7 +140,8 @@ Cold-start facts that are easy to rediscover the hard way: manifests for monorepo plugins; third-party entries keep their own `repo` URL and empty `plugin_path`. -More: `CONTRIBUTING.md`, `SUBMISSION.md`, `VERIFICATION.md`. +More: [CONTRIBUTING.md](./CONTRIBUTING.md), [SUBMISSION.md](./SUBMISSION.md), +[VERIFICATION.md](./VERIFICATION.md). --- @@ -155,7 +156,7 @@ A change is **correct** only if all of the following hold for touched plugins: | Manifest valid | Missing required fields / schema drift | CI vs core `schema/manifest_schema.json` | | Renders on panels | Crash, draw past edge, or golden drift | Core `check_plugin.py` + CI harness | | Config UI ↔ runtime | Schema default ≠ `config.get` default; README tables lie | Manual + PR checklist; prefer matching schema | -| No secret leak | Key/token committed | `.gitignore` + PR template / VERIFICATION | +| No secret leak | Key/token committed | `.gitignore` + PR template / [VERIFICATION.md](./VERIFICATION.md) | Optional but strong: commit `plugins//test/harness.json` + golden PNGs so visual drift fails CI instead of showing up on a Pi. diff --git a/SUBMISSION.md b/SUBMISSION.md index acaea2d4..d6c70ae6 100644 --- a/SUBMISSION.md +++ b/SUBMISSION.md @@ -66,8 +66,10 @@ For monorepo submissions (Option A), CI runs automatically on your PR: 1. **Automated CI gates**: - **Version bump** enforced on any changed plugin code - **Manifest schema validation** against the core `manifest_schema.json` - - **Safety harness** — renders every screen at every matrix size (64×32, - 128×32, 128×64, 256×32), failing on crashes or content past the edge + - **Safety harness** — renders every screen across the harness size matrix + (design for 64×32, 128×32, 128×64, 256×32; CI may include additional sizes — + see `docs/plugin-development/07-testing-ci-and-registry.md`), failing on + crashes or content past the edge - **Module-collision check** across all plugins 2. **Code Review**: Manual review of plugin code 3. **Testing**: Installation and basic functionality diff --git a/VERIFICATION.md b/VERIFICATION.md index c82bded9..8a91eac1 100644 --- a/VERIFICATION.md +++ b/VERIFICATION.md @@ -94,8 +94,9 @@ module-collision check. Use this list for the human judgment CI can't make ## Testing - [ ] Tested on Raspberry Pi -- [ ] Renders correctly at all four harness sizes (64×32, 128×32, 128×64, - 256×32) — the safety harness checks this automatically +- [ ] Renders correctly across the safety harness sizes (design for the classic + four — 64×32, 128×32, 128×64, 256×32; CI also covers additional panels — + see [docs/plugin-development/07-testing-ci-and-registry.md](docs/plugin-development/07-testing-ci-and-registry.md)) - [ ] No excessive CPU/memory usage - [ ] No crashes or freezes diff --git a/docs/plugin-development/05-adaptive-layout.md b/docs/plugin-development/05-adaptive-layout.md index dbd82a00..48d3eb0f 100644 --- a/docs/plugin-development/05-adaptive-layout.md +++ b/docs/plugin-development/05-adaptive-layout.md @@ -2,11 +2,12 @@ [← Guide index](./README.md) · [← Styling & skins](./04-styling-and-skins.md) -Every plugin must render correctly on **all four supported panel sizes** — +Every plugin must render correctly on the **classic panel sizes** — 64×32, 128×32, 128×64, and 256×32 — with nothing drawn past the edge. The [safety harness](./07-testing-ci-and-registry.md#the-safety-harness) enforces -this on every PR. This page covers how plugins adapt to size, from simple -tier-branching up to the core's opt-in `adaptive` layout engine. +this on every PR (and by default also exercises additional sizes — see topic 07). +This page covers how plugins adapt to size, from simple tier-branching up to the +core's opt-in `adaptive` layout engine. --- diff --git a/docs/plugin-development/README.md b/docs/plugin-development/README.md index 1ec58a7c..09eb44ad 100644 --- a/docs/plugin-development/README.md +++ b/docs/plugin-development/README.md @@ -45,7 +45,8 @@ These trip up nearly every first plugin. Each is expanded in the topic pages: 5. **Give deferred/subpackage modules plugin-unique names.** Two plugins sharing a bare module name (`data_model.py`) can bind each other's module and fail to load. See [topic 7](./07-testing-ci-and-registry.md#module-collisions). -6. **Render correctly at every matrix size** (64×32, 128×32, 128×64, 256×32). +6. **Render correctly at every matrix size** (start with 64×32, 128×32, 128×64, + 256×32; the harness may test more — see topic 07). The safety harness enforces it. See [topic 5](./05-adaptive-layout.md) and [topic 7](./07-testing-ci-and-registry.md#the-safety-harness). From 5b22aabbe75d5fda2915fab084a0ad8831555a0f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 17:12:09 -0400 Subject: [PATCH 3/3] fix(ci): use ledmatrix_min_version in the entries this PR adds The safety job failed on the manifest-version gate: - mqtt-notifications: versions[0] (1.1.1) uses the deprecated 'ledmatrix_min'. Rename it to 'ledmatrix_min_version' - on-air: versions[0] (1.2.5) uses the deprecated 'ledmatrix_min' 2 problem(s). That is the key the store and loader actually read, so a floor declared under the old name resolves to no floor at all. Renamed in place in both new entries, preserving key order; nothing else in either manifest moves and no version is bumped, since only a key name changed. Verified with the same two commands CI runs: check_manifest_version_fields.py OK: 6 plugin(s) checked, newest version entries are current. test_check_manifest_version_fields.py All 12 cases passed. Not fixed here: ten other plugins still carry 'ledmatrix_min' in their newest entry (christmas-countdown, clock-simple, hello-world, incoming-packages, jellyfin-now-playing, olympics, static-image, text-display, tide-display, web-ui-info). The gate only inspects plugins a PR changes, so they pass until touched. Fixing them means bumping ten unrelated plugins, which does not belong in this PR. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW --- plugins/mqtt-notifications/manifest.json | 2 +- plugins/on-air/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/mqtt-notifications/manifest.json b/plugins/mqtt-notifications/manifest.json index 21a06b24..2a163af6 100644 --- a/plugins/mqtt-notifications/manifest.json +++ b/plugins/mqtt-notifications/manifest.json @@ -22,7 +22,7 @@ "version": "1.1.1", "released": "2026-08-15", "notes": "Mark MQTT broker password as x-secret so the web UI masks it.", - "ledmatrix_min": "2.0.0" + "ledmatrix_min_version": "2.0.0" }, { "version": "1.1.0", diff --git a/plugins/on-air/manifest.json b/plugins/on-air/manifest.json index f37137be..cdfa8109 100644 --- a/plugins/on-air/manifest.json +++ b/plugins/on-air/manifest.json @@ -26,7 +26,7 @@ "version": "1.2.5", "released": "2026-08-15", "notes": "Also mark mqtt_password as x-secret (in addition to x-sensitive) so the web UI masks it consistently with other plugins.", - "ledmatrix_min": "2.0.0" + "ledmatrix_min_version": "2.0.0" }, { "version": "1.2.4",