Files
LEDMatrix/CLAUDE.md
T
cdf03fb107 Add skin system: user-installable visual overlays for sports scoreboards (#419)
* Add skin system: user-installable visual overlays for sports scoreboards

Skins restyle a scoreboard's live/recent/upcoming rendering while the
host plugin keeps doing data fetching, scheduling, caching, live
priority, and vegas mode — the anti-fork alternative for users who only
want a different layout.

- src/skin_system/: ScoreboardSkin API, SkinContext (canvas + adaptive
  layout + logo/font helpers), discovery/loading runtime with API major
  version gating and per-skin module namespacing
- src/base_classes/sports.py: _render_game() seam at the three
  _draw_scorebug_layout call sites; skin-first with built-in fallback,
  3-strikes session disable, slow-render warning; per-mode skin config
- scripts/validate_skin.py: headless multi-mode/multi-size validator
  with bundled per-sport fixtures (no hardware or network needed)
- skins/example-classic-baseball/: working reference skin
- Web UI: served-schema Visual Skin dropdown (validation never
  enum-restricted, so uninstalled skins can't invalidate configs) and
  GET /api/v3/skins
- Store: registry entries with type "skin" install to skins/
- docs/SKIN_SYSTEM.md (architecture), docs/CREATING_SKINS.md (author
  guide incl. Claude Code prompt), view-model contract locked by tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LrCusPasy1qeUN5anK3aA1

* Address review feedback on skin system

- skin_runtime: cache the entry module so the 2nd/3rd load of the same
  skin (live/recent/upcoming hosts) doesn't re-execute it with unbound
  sibling aliases; rebind cached sibling modules to their bare names
  around entry import and restore prior bindings after; include per-
  manifest mtimes in the discovery cache fingerprint so in-place skin
  updates are picked up
- sports.py: count render_skin_card exceptions toward the 3-strike
  session disable
- store_manager: validate skin ids (pattern + resolved-path containment
  in skins/), reject registry/manifest id mismatches, and stage+validate
  downloads in a temp sibling before replacing an existing skin
- schema_manager: leave the schema untouched when the configured skin
  value is a per-mode mapping (a string dropdown could overwrite it)
- validate_skin.py: reject non-positive sizes and non-object --options
  at parse time; support --output-dir outside the repo; type annotations
- example skin: validate accent_color once at load with logged fallback
- fixtures: pregame 0-0 scores in football/hockey upcoming fixtures
- api /skins: rely on the self-invalidating discovery cache instead of
  force_refresh
- docs: valid JSON manifest example, load_logo caching semantics spelled
  out, language ids on fenced blocks
- tests: view-model contract test now exercises the real extractor;
  regression test for repeated same-skin loads with sibling modules

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LrCusPasy1qeUN5anK3aA1

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-18 11:07:08 -04:00

3.1 KiB

LEDMatrix

Project Structure

  • src/plugin_system/ — Plugin loader, manager, store manager, base plugin class
  • web_interface/ — Flask web UI (blueprints, templates, static JS)
  • config/config.json — User plugin configuration (persists across plugin reinstalls)
  • plugin-repos/Default plugin install directory used by the Plugin Store, set by plugin_system.plugins_directory in config.json (default per config/config.template.json:130). Not gitignored.
  • plugins/ — Legacy/dev plugin location. Gitignored (plugins/*). Used by scripts/dev/dev_plugin_setup.sh for symlinks. The plugin loader falls back to it when something isn't found in plugin-repos/ (src/plugin_system/schema_manager.py:77).

Plugin System

  • Plugins inherit from BasePlugin in src/plugin_system/base_plugin.py
  • Required abstract methods: update(), display(force_clear=False)
  • Each plugin needs: manifest.json, config_schema.json, manager.py, requirements.txt
  • Plugin instantiation args: plugin_id, config, display_manager, cache_manager, plugin_manager
  • Config schemas use JSON Schema Draft-7
  • Display dimensions: always read dynamically from self.display_manager.matrix.width/height

Plugin Store Architecture

  • Official plugins live in the ledmatrix-plugins monorepo (not individual repos)
  • Plugin repo naming convention: ledmatrix-<plugin-id> (e.g., ledmatrix-football-scoreboard)
  • 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

Skin System (visual overlays for sports scoreboards)

  • Skins live in skins/<skin-id>/ (skin.json + skin.py), NOT in plugin dirs — plugin reinstall deletes plugin dirs
  • Core: src/skin_system/ (ScoreboardSkin, SkinContext, runtime); hook: SportsCore._render_game() in src/base_classes/sports.py
  • Skins render onto ctx.canvas only; fallback to built-in renderer on False/exception (3 strikes disables for session)
  • View-model guaranteed keys are frozen (see test/test_skin_system.py::TestViewModelContract) — renaming keys in _extract_game_details_common or sport extractors breaks published skins
  • Validate skins headlessly: python scripts/validate_skin.py --skin <id>; docs: docs/SKIN_SYSTEM.md, docs/CREATING_SKINS.md
  • Skins are NOT monorepo plugins: no manifest bump / update_registry.py needed

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