The web process built its own PluginManager and loaded plugins into itself: store installs and updates loaded or reloaded a web-side copy, and config saves and enable/disable called on_config_change, on_enable and on_disable on it. None of that reached the panel, and /plugins/installed reported runtime state from those copies. - Add PluginCatalog (src/plugin_system/plugin_catalog.py): manifests, directories, display modes, installed version, schema and config reads, with no way to run a plugin. app.py and both blueprints use it; the plugin_manager blueprint attribute is gone. - Remove every lifecycle call from the web routes. Config changes already reach the display through ConfigService (on_config_change) and the enabled-set reconcile. - Health and metrics readers move to api_v3.health_tracker / resource_monitor. /plugins/installed reports loaded/state/error_info as null (the display does not publish them) and enabled by the display's rule. - Store install, update and uninstall answer restart_required when the running display will not pick the change up by itself (display_restart_required). The restart banner follows the flag via window.noteRestartRequired instead of the /config/main URL heuristic; /config/main now sends restart_required: true. - The one remaining in-process import of plugin code (Starlark helper modules, oauth_flow action scripts) goes through _import_plugin_code_in_web_process() until a web-entry contract. - /plugins/installed reports vegas_participation (from #682) from the user's setting or the manifest, with vegas_participation_source; when only the plugin's code decides it, null with source 'runtime', since the web process no longer has plugin instances to ask. - Check & Update All keeps its restart flags when the final list refresh fails, and asks for a restart when an enabled plugin's first request got no answer and the re-sent one found it up to date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LED Matrix Web Interface V3
Modern, production web interface for controlling the LED Matrix display.
Overview
This directory contains the active V3 web interface with the following features:
- Real-time display preview via Server-Sent Events (SSE)
- Plugin management and configuration
- System monitoring and logs
- Modern, responsive UI
- RESTful API
Directory Structure
web_interface/
├── app.py # Main Flask application
├── start.py # Startup script
├── requirements.txt # Python dependencies
├── blueprints/ # Flask blueprints
│ ├── api_v3/ # API endpoints (package: config, display,
│ │ # plugins, system, backup, fonts, misc,
│ │ # wifi, starlark)
│ └── pages_v3.py # Page routes
├── tailwind/ # Tailwind config + input CSS (build inputs,
│ # not served; see "Styling" below)
├── templates/ # HTML templates
│ └── v3/
│ ├── base.html
│ └── partials/
└── static/ # CSS/JS assets
└── v3/
├── tailwind.css # GENERATED utility classes (committed)
├── plugin-frame.css # GENERATED styles for plugin web_ui/ iframes
├── app.css # hand-written: tokens, components, dark theme
├── app.js
├── manifest.json # PWA manifest
├── plugins_manager.js
├── icons/ # PWA / touch icons
├── js/ # Alpine, htmx, app shell, widgets, utils
└── vendor/ # codemirror, fontawesome
Styling (Tailwind CSS)
Templates and JS use Tailwind utility classes. The CSS for them is generated on a dev machine or in CI and committed, so the Pi never builds anything and the UI needs no CDN (it has to work in AP mode, with no internet).
static/v3/tailwind.cssholds the utilities. It is generated from the classes found intemplates/v3/,static/v3/**/*.jsandblueprints/, so it only contains what the UI uses.static/v3/app.cssis hand-written: theme tokens, base element styles, components (.btn,.card,.nav-tab, ...) and the dark theme ([data-theme="dark"] ...overrides).base.htmlloads it aftertailwind.css, so its rules win over utilities of equal specificity. Don't add utility classes to it; use the class and rebuild.static/v3/plugin-frame.cssstyles pluginweb_ui/fragments served by/v3/plugin-ui/<plugin>/web-ui/<file>in an iframe. Their markup lives in plugin repos, so it can't be scanned; its config safelists the common utility families instead.
After changing a template, a static JS file or anything in tailwind/,
rebuild and commit the CSS with your change:
python3 scripts/build_css.py # rewrites tailwind.css and plugin-frame.css
python3 scripts/build_css.py --check # what CI runs: fails if they are stale
No Node or npm is needed. The script downloads Tailwind's standalone CLI
(pinned version, SHA-256 checked) for your OS once and caches it outside
the repo (LEDMATRIX_TAILWIND_CACHE overrides where). CI runs --check
on every PR.
Where to change what:
- A class built at runtime (
`bg-${color}-100`) is invisible to the scanner: add it tosafelistintailwind/tailwind.config.js, or better, write the full class names in the code. - Colours, font sizes and shadows that differ from stock Tailwind (darker
gray text, emerald/amber button fills, token-based shadows) are set in the
themeoftailwind/tailwind.config.js. - Dark mode is the
data-theme="dark"attribute on<html>; thedark:variant is configured to match it.
Running the Web Interface
Standalone (Development)
From the project root:
python3 web_interface/start.py
As a Service (Production)
The web interface can run as a systemd service that starts automatically based on the web_display_autostart configuration setting:
sudo systemctl start ledmatrix-web
sudo systemctl enable ledmatrix-web # Start on boot
Accessing the Interface
Once running, access the web interface at:
- Local: http://localhost:5000
- Network: http://:5000
Configuration
The web interface reads configuration from:
config/config.json- Main configurationconfig/config_secrets.json- API keys and secrets
API Documentation
The V3 API is the api_v3 blueprint, registered at /api/v3/ in
app.py. For the complete
list and request/response formats, see
docs/REST_API_REFERENCE.md. Quick
reference for the most common endpoints:
Configuration
GET /api/v3/config/main- Get main configurationPOST /api/v3/config/main- Save main configurationGET /api/v3/config/secrets- Get secrets configurationPOST /api/v3/config/raw/main- Save raw main config (Config Editor)POST /api/v3/config/raw/secrets- Save raw secrets
Display & System Control
GET /api/v3/system/status- System statusPOST /api/v3/system/action- Control display (action body:start_display,stop_display,restart_display_service,restart_web_service,git_pull,reboot_system,shutdown_system,enable_autostart,disable_autostart)GET /api/v3/display/current- Current display frameGET /api/v3/display/on-demand/status- On-demand statusPOST /api/v3/display/on-demand/start- Trigger on-demand displayPOST /api/v3/display/on-demand/stop- Clear on-demand
Plugins
GET /api/v3/plugins/installed- List installed pluginsGET /api/v3/plugins/config?plugin_id=<id>- Get plugin configPOST /api/v3/plugins/config- Update plugin configurationGET /api/v3/plugins/schema?plugin_id=<id>- Get plugin schemaPOST /api/v3/plugins/toggle- Enable/disable pluginPOST /api/v3/plugins/install- Install from registryPOST /api/v3/plugins/install-from-url- Install from GitHub URLPOST /api/v3/plugins/uninstall- Uninstall pluginPOST /api/v3/plugins/update- Update plugin
Plugin Store
GET /api/v3/plugins/store/list- List available registry pluginsGET /api/v3/plugins/store/github-status- GitHub authentication statusPOST /api/v3/plugins/store/refresh- Refresh registry from GitHub
Real-time Streams (SSE)
SSE stream endpoints are defined directly on the Flask app in app.py
(stream_stats, stream_display, stream_logs, followed by their CSRF
exemption and rate-limit hookup), not on the api_v3 blueprint:
GET /api/v3/stream/stats- System statistics streamGET /api/v3/stream/display- Display preview streamGET /api/v3/stream/logs- Service logs stream
Development
When making changes to the web interface:
- Edit files in this directory
- Test changes by running
python3 web_interface/start.py - Restart the service if running:
sudo systemctl restart ledmatrix-web
Notes
- Templates and static files use the
v3/prefix to allow for future versions - The interface uses Flask blueprints for modular organization
- SSE streams provide real-time updates without polling