Files
LEDMatrix/docs/DEVELOPER_QUICK_REFERENCE.md
Chuck 781224591f fix: post-audit follow-up code fixes (cache, fonts, icons, dev script) (#307)
* fix: post-audit follow-up code fixes (cache, fonts, icons, dev script, CI)

The docs refresh effort (#306, ledmatrix-plugins#92) surfaced seven
code bugs that were intentionally left out of the docs PRs because
they required code changes rather than doc fixes. This PR addresses
the six that belong in LEDMatrix (the seventh — a lacrosse-scoreboard
mode rename — lives in the plugins repo).

Bug 1: cache_manager.delete() AttributeError
  src/common/api_helper.py:287 and
  src/plugin_system/resource_monitor.py:343 both call
  cache_manager.delete(key), which doesn't exist — only
  clear_cache(key=None). Added a delete() alias method on
  CacheManager that forwards to clear_cache(key). Reverts the
  "There is no delete() method" wording in DEVELOPER_QUICK_REFERENCE,
  .cursorrules so the docs match the new shim.

Bug 2: dev_plugin_setup.sh PROJECT_ROOT resolution
  scripts/dev/dev_plugin_setup.sh:9 set PROJECT_ROOT to SCRIPT_DIR
  instead of walking up two levels to the repo root, so PLUGINS_DIR
  resolved to scripts/dev/plugins/ and created symlinks under the
  script's own directory. Fixed the path and removed the stray
  scripts/dev/plugins/of-the-day symlink left by earlier runs.

Bug 3: plugin custom icons regressed from v2 to v3
  web_interface/blueprints/api_v3.py built the /plugins/installed
  response without including the manifest's "icon" field, and
  web_interface/templates/v3/base.html hardcoded
  fas fa-puzzle-piece in all three plugin-tab render sites. Pass
  the icon through the API and read it from the templates with a
  puzzle-piece fallback. Reverts the "currently broken" banners in
  docs/PLUGIN_CUSTOM_ICONS.md and docs/PLUGIN_CUSTOM_ICONS_FEATURE.md.

Bug 4: register_plugin_fonts was never wired up
  src/font_manager.py:150 defines register_plugin_fonts(plugin_id,
  font_manifest) but nothing called it, so plugin manifests with a
  "fonts" block were silently no-ops. Wired the call into
  PluginManager.load_plugin() right after plugin_loader.load_plugin
  returns. Reverts the "not currently wired" warning in
  docs/FONT_MANAGER.md's "For Plugin Developers" section.

Bug 5: dead web_interface_v2 import pattern (LEDMatrix half)
  src/base_odds_manager.py had a try/except importing
  web_interface_v2.increment_api_counter, falling back to a no-op
  stub. The module doesn't exist anywhere in the v3 codebase and
  no API metrics dashboard reads it. Deleted the import block and
  the single call site; the plugins-repo half of this cleanup lands
  in ledmatrix-plugins#<next>.

Bug 7: no CI test workflow
  .github/workflows/ only contained security-audit.yml; pytest ran
  locally but was not gated on PRs. Added
  .github/workflows/tests.yml running pytest against Python 3.10,
  3.11, 3.12 in EMULATOR=true mode, skipping tests marked hardware
  or slow. Updated docs/HOW_TO_RUN_TESTS.md to reflect that the
  workflow now exists.

Verification done locally:
  - CacheManager.delete(key) round-trips with set/get
  - base_odds_manager imports without the v2 module present
  - dev_plugin_setup.sh PROJECT_ROOT resolves to repo root
  - api_v3 and plugin_manager compile clean
  - tests.yml YAML parses
  - Script syntax check on dev_plugin_setup.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on #307

- src/cache_manager.py: clear_cache(key) treated empty string as
  "wipe all" because of `if key:`. Switched to `key is None`
  branching, made delete(key) and clear_cache(key) reject empty
  strings and None outright with ValueError, and updated both
  docstrings to make the contract explicit. Verified locally
  with a round-trip test that clear_cache() (no arg) still
  wipes everything but clear_cache("") and delete("") raise.

- src/plugin_system/plugin_manager.py: was reaching for the
  font manager via getattr(self.display_manager, 'font_manager',
  None). PluginManager already takes a dedicated font_manager
  parameter (line 54) and stores it as self.font_manager
  (line 69), so the old path was both wrong and could miss the
  font manager entirely when the host injects them separately.
  Switched to self.font_manager directly with the same try/except
  warning behavior.

- web_interface/templates/v3/base.html: in the full plugin-tab
  renderer, the icon was injected with
  `<i class="${escapeHtml(plugin.icon)}">` — but escapeHtml only
  escapes <, >, and &, not double quotes, so a manifest with a
  quote in its icon string could break out of the class
  attribute. Replaced the innerHTML template with createElement
  for the <i> tag, set className from plugin.icon directly
  (no string interpolation), and used a text node for the
  label. Same fix shape would also harden the two stub-renderer
  sites at line 515 / 774, but those already escape `"` to
  &quot; and CodeRabbit only flagged this site, so leaving them
  for now.

- docs/FONT_MANAGER.md: clarified that the Manual Font Overrides
  *workflow* (set_override / remove_override / font_overrides.json)
  is the supported override path today, and only the Fonts tab
  in the web UI is the placeholder. Previous wording conflated
  the two and made it sound like overrides themselves were
  broken.

- docs/HOW_TO_RUN_TESTS.md: replaced the vague "see the PR
  adding it" with a concrete link to #307 and a note that the
  workflow file itself is held back pending the workflow scope.

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>
2026-04-08 09:25:12 -04:00

5.8 KiB

Developer Quick Reference

One-page quick reference for common LEDMatrix development tasks.

REST API Endpoints

Most Common Endpoints

# Get installed plugins
GET /api/v3/plugins/installed

# Get plugin configuration
GET /api/v3/plugins/config?plugin_id=<plugin_id>

# Save plugin configuration
POST /api/v3/plugins/config
{"plugin_id": "my-plugin", "config": {...}}

# Start on-demand display
POST /api/v3/display/on-demand/start
{"plugin_id": "my-plugin", "duration": 30}

# Get system status
GET /api/v3/system/status

# Execute system action
POST /api/v3/system/action
{"action": "start_display"}

Base URL: http://your-pi-ip:5000/api/v3

See API_REFERENCE.md for complete documentation.

Display Manager Quick Methods

# Core operations
display_manager.clear()                    # Clear display
display_manager.update_display()           # Update physical display

# Text rendering
display_manager.draw_text("Hello", x=10, y=16, color=(255, 255, 255))
display_manager.draw_text("Centered", centered=True)  # Auto-center

# Utilities
width = display_manager.get_text_width("Text", font)
height = display_manager.get_font_height(font)

# Weather icons
display_manager.draw_weather_icon("rain", x=10, y=10, size=16)

# Scrolling state
display_manager.set_scrolling_state(True)
display_manager.defer_update(lambda: self.update_cache(), priority=0)

Cache Manager Quick Methods

# Basic caching
cached = cache_manager.get("key", max_age=3600)
cache_manager.set("key", data)
cache_manager.delete("key")       # alias for clear_cache(key)

# Advanced caching
data = cache_manager.get_cached_data_with_strategy("key", data_type="weather")
data = cache_manager.get_background_cached_data("key", sport_key="nhl")

# Strategy
strategy = cache_manager.get_cache_strategy("weather")
interval = cache_manager.get_sport_live_interval("nhl")

Plugin Manager Quick Methods

# Get plugins
plugin = plugin_manager.get_plugin("plugin-id")
all_plugins = plugin_manager.get_all_plugins()
enabled = plugin_manager.get_enabled_plugins()

# Get info
info = plugin_manager.get_plugin_info("plugin-id")
modes = plugin_manager.get_plugin_display_modes("plugin-id")

BasePlugin Quick Reference

class MyPlugin(BasePlugin):
    def update(self):
        # Fetch data (called based on update_interval)
        cache_key = f"{self.plugin_id}_data"
        cached = self.cache_manager.get(cache_key, max_age=3600)
        if cached:
            self.data = cached
            return
        self.data = self._fetch_from_api()
        self.cache_manager.set(cache_key, self.data)
    
    def display(self, force_clear=False):
        # Render display
        if force_clear:
            self.display_manager.clear()
        self.display_manager.draw_text("Hello", x=10, y=16)
        self.display_manager.update_display()
    
    # Optional methods
    def has_live_content(self) -> bool:
        return len(self.live_items) > 0
    
    def validate_config(self) -> bool:
        return "api_key" in self.config

Common Patterns

Caching Pattern

def update(self):
    cache_key = f"{self.plugin_id}_data"
    cached = self.cache_manager.get(cache_key, max_age=3600)
    if cached:
        self.data = cached
        return
    self.data = self._fetch_from_api()
    self.cache_manager.set(cache_key, self.data)

Error Handling Pattern

def display(self, force_clear=False):
    try:
        if not self.data:
            self._display_no_data()
            return
        self._render_content()
        self.display_manager.update_display()
    except Exception as e:
        self.logger.error(f"Display error: {e}", exc_info=True)
        self._display_error()

Scrolling Pattern

def display(self, force_clear=False):
    self.display_manager.set_scrolling_state(True)
    try:
        # Scroll content...
        for x in range(width, -text_width, -2):
            self.display_manager.clear()
            self.display_manager.draw_text(text, x=x, y=16)
            self.display_manager.update_display()
            time.sleep(0.05)
    finally:
        self.display_manager.set_scrolling_state(False)

Plugin Development Checklist

  • Plugin inherits from BasePlugin
  • Implements update() and display() methods
  • manifest.json with required fields
  • config_schema.json for web UI (recommended)
  • README.md with documentation
  • Error handling implemented
  • Uses caching appropriately
  • Tested on Raspberry Pi hardware
  • Follows versioning best practices

Common Errors & Solutions

Error Solution
Plugin not discovered Check manifest.json exists and id matches directory name
Import errors Check requirements.txt and dependencies
Config validation fails Verify config_schema.json syntax
Display not updating Call update_display() after drawing
Cache not working Check cache directory permissions

File Locations

LEDMatrix/
├── plugins/              # Installed plugins
├── config/
│   ├── config.json      # Main configuration
│   └── config_secrets.json  # API keys and secrets
├── docs/                 # Documentation
│   ├── API_REFERENCE.md
│   ├── PLUGIN_API_REFERENCE.md
│   └── ...
└── src/
    ├── display_manager.py
    ├── cache_manager.py
    └── plugin_system/
        └── base_plugin.py

Tip: Bookmark this page for quick access to common methods and patterns!