Files
LEDMatrix/docs/DEV_PREVIEW.md
Chuck 23f0176c18 feat: add dev preview server and CLI render script (#264)
* fix(web): wire up "Check & Update All" plugins button

window.updateAllPlugins was never assigned, so the button always showed
"Bulk update handler unavailable." Wire it to PluginInstallManager.updateAll(),
add per-plugin progress feedback in the button text, show a summary
notification on completion, and skip redundant plugin list reloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add dev preview server, CLI render script, and visual test display manager

Adds local development tools for rapid plugin iteration without deploying to RPi:

- VisualTestDisplayManager: renders real pixels via PIL (same fonts/interface as production)
- Dev preview server (Flask): interactive web UI with plugin picker, auto-generated config
  forms, zoom/grid controls, and mock data support for API-dependent plugins
- CLI render script: render any plugin to PNG for AI-assisted visual feedback loops
- Updated test runner and conftest to auto-detect plugin-repos/ directory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(dev-preview): address code review issues

- Use get_logger() from src.logging_config instead of logging.getLogger()
  in visual_display_manager.py to match project logging conventions
- Eliminate duplicate public/private weather draw methods — public draw_sun/
  draw_cloud/draw_rain/draw_snow now delegate to the private _draw_* variants
  so plugins get consistent pixel output in tests vs production
- Default install_deps=False in dev_server.py and render_plugin.py — dev
  scripts don't need to run pip install; developers are expected to have
  plugin deps installed in their venv already
- Guard plugins_dir fixture against PermissionError during directory iteration
- Fix PluginInstallManager.updateAll() to fall back to window.installedPlugins
  when PluginStateManager.installedPlugins is empty (plugins_manager.js
  populates window.installedPlugins independently of PluginStateManager)
- Remove 5 debug console.log statements from plugins_manager.js button setup
  and initialization code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(scroll): fix scroll completion to prevent multi-pass wrapping

Change required_total_distance from total_scroll_width + display_width to
total_scroll_width alone. The scrolling image already contains display_width
pixels of blank initial padding, so reaching total_scroll_width means all
content has scrolled off-screen. The extra display_width term was causing
1-2+ unnecessary wrap-arounds, making the same games appear multiple times
and producing a black flicker between passes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dev-preview): address PR #264 code review findings

- docs/DEV_PREVIEW.md: add bash language tag to fenced code block
- scripts/dev_server.py: add MAX/MIN_WIDTH/HEIGHT constants and validate
  width/height in render endpoint; add structured logger calls to
  discover_plugins (missing dirs, hidden entries, missing manifest,
  JSON/OS errors, duplicate ids); add type annotations to all helpers
- scripts/render_plugin.py: add MIN/MAX_DIMENSION validation after
  parse_args; replace prints with get_logger() calls; narrow broad
  Exception catches to ImportError/OSError/ValueError in plugin load
  block; add type annotations to all helpers and main(); rename unused
  module binding to _module
- scripts/run_plugin_tests.py: wrap plugins_path.iterdir() in
  try/except PermissionError with fallback to plugin-repos/
- scripts/templates/dev_preview.html: replace non-focusable div toggles
  with button role="switch" + aria-checked; add keyboard handlers
  (Enter/Space); sync aria-checked in toggleGrid/toggleAutoRefresh
- src/common/scroll_helper.py: early-guard zero total_scroll_width to
  keep scroll_position at 0 and skip completion/wrap logic
- src/plugin_system/testing/visual_display_manager.py: forward color
  arg in draw_cloud -> _draw_cloud; add color param to _draw_cloud;
  restore _scrolling_state in reset(); narrow broad Exception catches in
  _load_fonts to FileNotFoundError/OSError/ImportError; add explicit
  type annotations to draw_text
- test/plugins/test_visual_rendering.py: use context manager for
  Image.open in test_save_snapshot
- test/plugins/conftest.py: add return type hints to all fixtures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add bandit and gitleaks pre-commit hooks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:57:42 -05:00

5.2 KiB

Dev Preview & Visual Testing

Tools for rapid plugin development without deploying to the RPi.

Dev Preview Server

Interactive web UI for tweaking plugin configs and seeing the rendered display in real time.

Quick Start

python scripts/dev_server.py
# Opens at http://localhost:5001

Options

python scripts/dev_server.py --port 8080                          # Custom port
python scripts/dev_server.py --extra-dir /path/to/custom-plugin   # 3rd party plugins
python scripts/dev_server.py --debug                              # Flask debug mode

Workflow

  1. Select a plugin from the dropdown (auto-discovers from plugins/ and plugin-repos/)
  2. The config form auto-generates from the plugin's config_schema.json
  3. Tweak any config value — the display preview updates automatically
  4. Toggle "Auto" off for plugins with slow update() calls, then click "Render" manually
  5. Use the zoom slider to scale the tiny display (128x32) up for detailed inspection
  6. Toggle the grid overlay to see individual pixel boundaries

Mock Data for API-dependent Plugins

Many plugins fetch data from APIs (sports scores, weather, stocks). To render these locally, expand "Mock Data" and paste a JSON object with cache keys the plugin expects.

To find the cache keys a plugin uses, search its manager.py for self.cache_manager.set( calls.

Example for a sports plugin:

{
  "football_scores": {
    "games": [
      {"home": "Eagles", "away": "Chiefs", "home_score": 24, "away_score": 21, "status": "Final"}
    ]
  }
}

CLI Render Script

Render any plugin to a PNG image from the command line. Useful for AI-assisted development and scripted workflows.

Usage

# Basic — renders with default config
python scripts/render_plugin.py --plugin hello-world --output /tmp/hello.png

# Custom config
python scripts/render_plugin.py --plugin clock-simple \
  --config '{"timezone":"America/New_York","time_format":"12h"}' \
  --output /tmp/clock.png

# Different display dimensions
python scripts/render_plugin.py --plugin hello-world --width 64 --height 32 --output /tmp/small.png

# 3rd party plugin from a custom directory
python scripts/render_plugin.py --plugin my-plugin --plugin-dir /path/to/repo --output /tmp/my.png

# With mock API data
python scripts/render_plugin.py --plugin football-scoreboard \
  --mock-data /tmp/mock_scores.json \
  --output /tmp/football.png

Using with Claude Code / AI

Claude can run the render script, then read the output PNG (Claude is multimodal and can see images). This enables a visual feedback loop:

Claude → bash: python scripts/render_plugin.py --plugin hello-world --output /tmp/render.png
Claude → Read /tmp/render.png   ← Claude sees the actual rendered display
Claude → (makes code changes based on what it sees)
Claude → bash: python scripts/render_plugin.py --plugin hello-world --output /tmp/render2.png
Claude → Read /tmp/render2.png  ← verifies the visual change

VisualTestDisplayManager (for test suites)

A display manager that renders real pixels for use in pytest, without requiring hardware.

Basic Usage

from src.plugin_system.testing import VisualTestDisplayManager, MockCacheManager, MockPluginManager

def test_my_plugin_renders_title():
    display = VisualTestDisplayManager(width=128, height=32)
    cache = MockCacheManager()
    pm = MockPluginManager()

    plugin = MyPlugin(
        plugin_id='my-plugin',
        config={'enabled': True, 'title': 'Hello'},
        display_manager=display,
        cache_manager=cache,
        plugin_manager=pm
    )

    plugin.update()
    plugin.display(force_clear=True)

    # Verify pixels were drawn (not just that methods were called)
    pixels = list(display.image.getdata())
    assert any(p != (0, 0, 0) for p in pixels), "Display should not be blank"

    # Save snapshot for manual inspection
    display.save_snapshot('/tmp/test_my_plugin.png')

Pytest Fixture

A visual_display_manager fixture is available in plugin tests:

def test_rendering(visual_display_manager):
    visual_display_manager.draw_text("Test", x=10, y=10, color=(255, 255, 255))
    assert visual_display_manager.width == 128
    pixels = list(visual_display_manager.image.getdata())
    assert any(p != (0, 0, 0) for p in pixels)

Key Differences from MockDisplayManager

Feature MockDisplayManager VisualTestDisplayManager
Renders pixels No (logs calls only) Yes (real PIL rendering)
Loads fonts No Yes (same fonts as production)
Save to PNG No Yes (save_snapshot())
Call tracking Yes Yes (backwards compatible)
Use case Unit tests (method call assertions) Visual tests, dev preview

Plugin Test Runner

The test runner auto-detects plugin-repos/ for monorepo development:

# Auto-detect (tries plugins/ then plugin-repos/)
python scripts/run_plugin_tests.py

# Test specific plugin
python scripts/run_plugin_tests.py --plugin clock-simple

# Explicit directory
python scripts/run_plugin_tests.py --plugins-dir plugin-repos/

# With coverage
python scripts/run_plugin_tests.py --coverage --verbose