Findings from the automated review of #604. Symlinks (CWE-59, the serious one). A root chown that follows links is a privilege-escalation primitive: anyone able to write in starlark-apps could point a link at a root-owned file and have the repair hand it over. Entries are now read with os.lstat, symlinks are skipped outright, and the chown passes follow_symlinks=False. Descendants are processed before the directory itself, so the container does not change hands while its contents are still being walked. install_app() caught PermissionError in its broad handler and returned False, which both routes report as a generic install failure -- the exact shape of the bug this PR exists to fix, since the caller could not tell "this app is broken" from "this process cannot write here". PermissionError is now re-raised; every other failure still returns False. The test fixtures skipped on bare Exception, which would have turned a syntax error or NameError in the plugin into a green run. They now skip only for a named absent dependency and re-raise anything else. Also fixed the _Stat stub that failed in CI but passed locally: it carried only st_uid/st_gid, and pathlib reads st_mode while walking. It now wraps the real stat result and overrides ownership alone. NOT taken: the CodeQL "information exposure through an exception" finding on the hint response. Dropping `details` would contradict this package's documented rule -- "if it returns 5xx, it says why" -- which test_no_api_v3_handler_discards_its_exception enforces with an allowance that may shrink and never grow. The Starlark routes are the ones that policy was written for: they answered 500 with no detail for three releases. describe_exception already redacts credentials and truncates. Keeping the detail is the deliberate trade-off, so the finding is declined rather than silently worked around. Verified on hdpi with the updated code: a symlink to /etc/shadow planted in starlark-apps was skipped while the directory was handed back, and /etc/shadow stayed root:shadow. Mutation-checked all three behaviours. The symlink test was vacuous on the first attempt -- the link already had the target owner, so it was skipped for the wrong reason and the mutation passed. It now forces the link to look like it needs handing over, and fails when the check is removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
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
├── run.sh # Shell runner 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
├── templates/ # HTML templates
│ └── v3/
│ ├── base.html
│ ├── index.html
│ └── partials/
└── static/ # CSS/JS assets
└── v3/
├── app.css
├── app.js
├── manifest.json # PWA manifest
├── plugins_manager.js
├── icons/ # PWA / touch icons
├── js/ # Alpine, htmx, app shell, widgets, utils
└── vendor/ # codemirror, fontawesome
Running the Web Interface
Standalone (Development)
From the project root:
python3 web_interface/start.py
Or using the shell script:
./web_interface/run.sh
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