Files
LEDMatrix/docs/WEB_INTERFACE_GUIDE.md
ChuckandClaude Opus 5.5 3967a6cffc fix(security): re-harden root sudo helpers; installer fixes; ARCHITECTURE and PERMISSIONS docs (#640)
* docs: add ARCHITECTURE and PERMISSIONS guides

ARCHITECTURE.md maps the processes, the state the display and web
services share through the cache, the display loop, the plugin system,
the web UI and the update path, with links into the code and a
where-to-start table.

PERMISSIONS.md lists who owns what after install, both sudoers files
(and why iptables is not granted), the polkit rule, and which
scripts/fix_perms script to run as which user.

Both are linked from the docs index, along with the MQTT bridge README
and src/common/README.md.

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

* docs: correct stale setup, service and troubleshooting claims

- README: quick actions run systemctl on ledmatrix.service (run.py), not
  display_controller.py; use_short_date_format has no effect; the
  installer uses system pip with --break-system-packages, not a venv.
- CONFIG_DEBUGGING: LEDMATRIX_DEBUG must be "true"; logs are in journald.
- GETTING_STARTED, WEB_INTERFACE_GUIDE, TROUBLESHOOTING: enabling a
  plugin, plugin settings, brightness and Vegas settings apply without a
  restart; matrix hardware settings still need one.
- TROUBLESHOOTING: install dependencies with sudo so the root service
  sees them; point permission problems at PERMISSIONS.md instead of a
  project-wide chown.
- ADVANCED_FEATURES: real BackgroundDataService stats keys; Vegas hooks
  return VegasDisplayMode and None falls back to capture; cache files
  are 0660; fix_web_permissions.sh runs as the web user and does not
  touch sudoers.
- STARLARK_APPS_GUIDE: only the linux-arm64 pixlet binary is downloaded.
- HOW_TO_RUN_TESTS: test class examples that exist.
- CLAUDE.md: PluginStoreManager, plugin_dirs.py, monorepo installs via
  the Trees API with ZIP fallback, requirements.txt is optional.

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

* docs: mark deprecated plugin APIs and state manifest fields once

Methods @deprecated("3.7.0") (the set pinned in test_deprecation.py)
were shown as current API in the quick reference, API reference,
advanced guide, development guide and FONT_MANAGER. Each is now marked
deprecated with its replacement. FONT_MANAGER is rewritten around the
current API; the override editor is gone and override methods are
deprecated.

Required manifest fields were stated three different ways. The API
reference now has one section: the 7 schema-required fields, the 4 the
store refuses without, class_name for the loader, and the 8 to set.
The other guides link to it.

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

* docs: document every src/common module and every widget

- src/common/README.md covered 7 of 17 modules. It now has a table of
  all of them (purpose, whether plugins import it, release to floor
  on), a short entry each, and logging advice that matches the code.
- SPORTS_UNIFICATION listed two shared modules and called
  sports_helpers the first; it now lists all six.
- The widgets README lists all 28 registered widgets plus the support
  files, and absorbs the parts that only docs/widget-guide.md had
  (x-options.labels, x-advanced, x-display hidden, plugin-file-manager).
  docs/widget-guide.md is now a pointer to it.

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

* fix(security): fix_web_permissions.sh re-hardens the root sudo helpers

The script chowns the whole project to the web user. That included
scripts/fix_perms/safe_plugin_rm.sh and safe_pip_install.sh -- the two
helpers /etc/sudoers.d/ledmatrix_web lets the web user run as root -- so
running it turned both into a root shell for whoever can edit them. It
also re-grouped config_secrets.json away from ledmatrix.

After the chown it now does what first_time_install.sh's Steps 11 and
11.1 do: helpers back to root:root 755, and config_secrets.json back to
the web unit's User=:ledmatrix 640. Each step is non-fatal and prints the
manual command if it fails.

Also fixes what the script and its docs claimed: it never configured
sudoers, its closing hint pointed at ./configure_web_sudo.sh (wrong
path), and the README and ADVANCED_FEATURES.md said to run it with sudo,
which it refuses.

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

* fix(security): validate and harden every sudoers drop-in the scripts write

configure_wifi_permissions.sh copied its rules into
/etc/sudoers.d/ledmatrix_wifi without `visudo -c`. A malformed drop-in
makes sudo refuse every command for every user, which on a headless Pi
leaves no way back in. It now checks first and leaves the installed file
alone when the rules do not parse, as the other two writers do. (It
already used mktemp, so that part of the review did not apply.)

It also grants the two literal commands wifi_manager.py runs for
NetworkManager's shared-mode dnsmasq drop-in -- `cp
/tmp/ledmatrix-nm-dnsmasq.conf .../dnsmasq-shared.d/ledmatrix-captive.conf`
and `rm -f` of that file. The directory's mkdir was granted, the file was
not. Both are pinned in test_sudo_allowlist_covers_calls.py.

configure_web_sudo.sh wrote its rules to /tmp/ledmatrix_web_sudoers_$$,
a predictable name in a world-writable directory; it now uses mktemp with
an EXIT trap, as first_time_install.sh does. It sets mode 440 on the
installed file instead of leaving the temp file's mode, and finds visudo
in /usr/sbin when that is not on the user's PATH, which skipped the
check silently.

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

* fix(install): escape the project path in the DNS-fix and MQTT unit renderers

install_dns_fix.sh and install_mqtt_bridge.sh substituted
__PROJECT_ROOT_DIR__ with the raw path, while the other three renderers
go through sed_escape_replacement from lib_systemd_render.sh. A checkout
under a path containing `&`, `\` or `|` rendered a corrupted unit from
these two only. Both now source the helper and use it, and a test checks
that every placeholder substitution in scripts/install uses an escaped
value.

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

* fix(install): stop the installer scripts reporting things that are not true

- first_time_install.sh printed "Password: ledmatrix123" for the setup
  access point. wifi_manager creates it as an open network ("No
  password" on the panel), so it now says so.
- Step 10.1 printed "✓ WiFi management permissions configured" straight
  after its own failure message; install_wifi_monitor.sh printed
  "✓ Package installation completed" after a failed apt install. The
  tick now only follows success.
- Step 7 printed "Web dependencies already installed ... in Step 5" in
  the one branch that runs because Step 5 did not install them, then
  created .web_deps_installed on that basis. It now warns and leaves the
  marker off so the next run retries, as the comment below it intends.
- check_system_compatibility.sh called Debian 12 Bookworm "full
  compatibility confirmed" while first_time_install.sh refuses anything
  but Debian 13. Bookworm, older Debian and non-Debian systems are now
  errors. Its counters used ((X++)), which under `set -e` exits the
  script at the first warning or error (the expression is 0), so the
  check never reached its summary on any system with one.
- configure_web_sudo.sh and configure_wifi_permissions.sh finished by
  testing `sudo -n test -f ...` and `sudo -n nmcli device status`,
  neither of which is granted, so they always reported a failure. They
  now ask `sudo -n -l` about commands the new rules do grant, which
  checks the rule without running anything.

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

* fix(install): print the completion summary before rebooting

With -y -- and so for every one-shot `curl | bash` install, which always
passes -y -- first_time_install.sh ran `reboot` about 180 lines before
its "Installation Complete / Web UI Access" summary. reboot returns at
once, so the summary printed while the Pi was going down and the SSH
session usually dropped before the web UI address could be read.

The reboot block moves, unchanged, to the very end of the script. The
interactive prompt now also follows the summary. Because the summary now
runs before the -y reboot, its one command that could fail under
`set -Eeuo pipefail` (the SSID lookup, when nmcli reports a connected
device but no active network line) gets `|| true`; a missing SSID was
already handled as "SSID unknown".

one-shot-install.sh prints its "Next steps" after the installer returns,
by which time the reboot is under way, so it now says so, and README's
Quick Install mentions the automatic reboot.

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

* chore(scripts): correct wrong comments and messages, drop dead code

No behaviour change except the output text noted below.

- 2775 is setgid, not the sticky bit (first_time_install.sh Step 3.1,
  fix_plugin_permissions.sh), and root needs no "PWM hardware access"
  to plugin files.
- The 777 comments in first_time_install.sh Step 3's fallback and
  fix_assets_permissions.sh said root needs it to write. Root ignores
  mode bits; the comments now say what 777 actually opens. The 777
  itself is unchanged.
- apt_remove ends in `|| true`, so Step 12's "Some packages could not be
  removed" branch could never run; it is gone and the helper stays
  non-fatal.
- detect_web_service_user's comment named Step 8 for the web unit
  (install_service.sh installs it in Step 7.5) and now says which
  branch actually runs.
- Step 5 described an "already installed" check that does not exist;
  the ACTUAL_USER comment described the re-exec backwards.
- on_error printed a literal "\n" before "Common fixes:".
- Dead code: one-shot-install.sh's uncalled fix_tmp_permissions,
  LEDMATRIX_ELEVATED=1 (never read) on the sudo re-exec, and
  configure_web_sudo.sh's unused PYTHON_PATH, which also made a missing
  python3 fatal for rules that never mention it.
- start_display.sh / stop_display.sh said "for user: <you>"; the
  service runs as root.

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

* refactor(fix_perms): fix_cache_permissions.sh uses setup_cache.sh's model

There were two models for /var/cache/ledmatrix. setup_cache.sh (the
installer's Step 2) and install_web_service.sh share it through the
ledmatrix group: root:ledmatrix, 2775, files 660, which is also what
DiskCache relies on to give files the directory's group.
fix_cache_permissions.sh instead made it 777 and re-grouped it to the
invoking user's group, undoing that.

It now runs setup_cache.sh for /var/cache/ledmatrix and keeps its own
handling of ~/.ledmatrix_cache. Dropped: /var/cache/ledmatrix/
placeholder_logos (nothing reads it) and the checks against the
`daemon` user (no service runs as daemon).

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

* ci: pin actions/checkout in the Claude workflows, drop template comments

claude.yml and claude-code-review.yml used actions/checkout@v4 while
test.yml and release-version-check.yml pin the v4.2.2 commit SHA; they
now pin the same SHA. The commented-out starter-template settings
(prompt, claude_args, paths, author filter) are removed.

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

* docs(scripts): index every script and list removal candidates

New scripts/README.md gives one line per top-level script and scripts
directory, marked keep, dev-only or diagnostic, and lists the eight
scripts nothing in the repo refers to as candidates for removal (kept
for now). The install, utils and dev READMEs now list the files they
were missing.

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

* test: tighten two checks that mutation testing showed were too loose

- The wifi sudoers check matched `visudo -c -f "$TEMP_SUDOERS"` in the
  error report too, so replacing the check with `if false` still passed.
  It now requires the command as the condition.
- The summary test never had the setup access point up, so reinstating
  the bogus "Password: ledmatrix123" line went unnoticed. A case with
  hostapd active now checks the AP is described as open.

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

* docs(permissions): describe the repaired fix_perms scripts and new WiFi grants

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

* docs(changelog): docs-scripts

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:31:41 -04:00

15 KiB
Raw Permalink Blame History

Web Interface Guide

Overview

The LEDMatrix web interface provides a complete control panel for managing your LED matrix display. Access all features through a modern, responsive web interface that works on desktop, tablet, and mobile devices.


Quick Start

Accessing the Interface

  1. Find your Raspberry Pi's IP address:

    hostname -I
    
  2. Open a web browser and navigate to:

    http://your-pi-ip:5000
    
  3. The interface will load with the Overview tab displaying system stats and a live display preview.

Note: If the interface doesn't load, verify the web service is running:

sudo systemctl status ledmatrix-web

Navigation

The interface uses a two-row tab layout. The system tabs are always present:

  • Overview — System stats, quick actions, live display preview
  • General — Timezone, location, plugin-system settings
  • WiFi — Network selection and AP-mode setup
  • Schedule — Power and dim schedules
  • Display — Matrix hardware configuration (rows, cols, hardware mapping, GPIO slowdown, brightness, PWM) and Vegas Scroll Mode settings
  • Rotation — drag-and-drop Rotation Order list and per-plugin Screen Durations
  • Config Editor — Raw config.json editor with validation
  • Backup & Restore — back up and restore your configuration
  • Fonts — Upload and manage fonts
  • Logs — Real-time log streaming
  • Cache — Cached data inspection and cleanup
  • Operation History — Recent service operations
  • Tools — system diagnostics, git & updates, Python dependencies, maintenance, power supply, network radio, services, and plugin health

A second nav row holds plugin tabs:

  • Plugin Manager — browse the Plugin Store section, install plugins from GitHub, enable/disable installed plugins
  • <plugin-id> — one tab per installed plugin for its own configuration form (auto-generated from the plugin's config_schema.json)

Features and Usage

Overview Tab

The Overview tab provides at-a-glance information and quick actions:

System Stats:

  • CPU usage and temperature
  • Memory usage
  • Disk usage
  • Network status

Quick Actions (verified in web_interface/templates/v3/partials/overview.html):

  • Start Display / Stop Display — control the display service
  • Restart Display Service — apply configuration changes
  • Restart Web Service — restart the web UI itself
  • Update Code — git pull the latest version (stashes local changes)
  • Reboot System / Shutdown System — confirm-gated power controls

Display Preview:

  • Live preview of what's currently shown on the LED matrix
  • Updates in real-time
  • Useful for remote monitoring

General Tab

Configure basic system settings:

  • Timezone — used by all time/date displays
  • Location — city/state/country for weather and other location-aware plugins
  • Plugin System Settings — including the plugins_directory (default plugin-repos/) used by the plugin loader
  • Web Display Autostart — whether the web interface service starts with the system (web_display_autostart)
  • Automatic updates — once a week, update LEDMatrix and every installed plugin with a newer version. Off by default. Runs 2–5 AM local time when possible, otherwise within a day of being due. The last result and next check are shown under the toggle, and anything other than success raises a banner on Overview.
    • Checks first: the code update is skipped, with the reason shown, if tracked files were edited locally (permission-only changes and edits under plugins/ or plugin-repos/ don't count; the pull carries those across and puts them back), the checkout has local commits, a rebase/merge is in progress, the branch has no upstream, less than 300 MB is free, or the newest version already failed once. A failed fetch is retried the next day.
    • Health check and rollback: after pulling, ledmatrix-update-verify.service restarts the services and checks that the web interface responds and the display (if it was running) stays up. If not — or if the new dependencies failed to install — it resets to the previous commit, reinstalls the previous dependencies and restarts again. A running display is restarted; a stopped one stays stopped.
    • Plugins update through the Plugin Store, which refuses versions that need a newer LEDMatrix and restores the old copy when an install fails. When the code changed, plugins wait until it passes its health check. Plugins are not health-checked after updating.
    • Setup needs no SSH. Turning the toggle on restarts the display service, which installs the health check (ledmatrix-update-verify.path and .service); the General tab shows when it is ready, or why setup failed. Until then only plugins update. New installs set it up during installation and can switch updates on with first_time_install.sh --enable-auto-update (or LEDMATRIX_AUTO_UPDATE=1, which one-shot-install.sh passes through), or at the installer's prompt.

Click Save to write changes to config/config.json. Most changes require a display service restart from Overview.

Display Tab

Configure your LED matrix hardware:

Matrix configuration:

  • rows — LED rows per panel (typically 32 or 64; even, at least 8 — the current rgbmatrix library rejects more than 64)
  • cols — LED columns per panel (typically 64 or 96; at least 16)
  • chain_length — number of horizontally chained panels
  • parallel — number of parallel chains (1–3)
  • hardware_mapping — adafruit-hat-pwm (with PWM jumper mod), adafruit-hat (without), regular (direct wiring, and the Adafruit Triple LED Matrix Bonnet), or regular-pi1
  • gpio_slowdown — depends on your Pi and panel (roughly 1–3 on a Pi 3, 2–4 on a Pi 4); raise it if rows jump or the image is garbage
  • brightness — 1–100%
  • pwm_bits, pwm_lsb_nanoseconds, pwm_dither_bits — PWM tuning
  • Dynamic Duration — global cap for plugins that extend their display time based on content

The collapsed Advanced Hardware & Display Options section holds multiplexing, panel type, row address type, scan mode, PWM tuning, the refresh-rate cap and hardware pulsing. Every field has a help tip, and the README's Display Settings section describes each one with its allowed range.

Vegas Scroll Mode: the Display tab also has a full Vegas Scroll Mode section — enable toggle, scroll speed, separator width, dynamic duration, and related settings — so you can configure Vegas mode entirely from the web UI without hand-editing JSON. See ADVANCED_FEATURES.md for what the options do.

Brightness and the Vegas Scroll settings apply to the running display within a few seconds. Matrix hardware settings (rows, columns, chain length, mapping, GPIO slowdown, PWM and refresh settings) are only read when the display starts, so those need Restart Display Service from the Overview tab.

Plugin Manager Tab

The Plugin Manager has three main sections:

  1. Installed Plugins — toggle installed plugins on/off, see version info. Each installed plugin also gets its own tab in the second nav row for its configuration form.
  2. Plugin Store — browse plugins from the official ledmatrix-plugins registry. Click Install to fetch and install. Filter by category and search.
  3. Install from GitHub — install third-party plugins by pasting a GitHub repository URL. Install Single Plugin for a single-plugin repo, Load Registry for a multi-plugin monorepo.

When a plugin is installed and enabled:

  • A new tab for that plugin appears in the second nav row
  • Open the tab to edit its config (auto-generated form from config_schema.json)
  • The tab also exposes Run On-Demand / Stop On-Demand controls to render that plugin immediately, even if it's disabled in the rotation

Per-plugin Configuration Tabs

Each installed plugin has its own tab in the second nav row. The form fields are auto-generated from the plugin's config_schema.json, so options always match the plugin's current code.

To temporarily run a plugin outside the normal rotation, use the Run On-Demand / Stop On-Demand buttons inside its tab. This works even when the plugin is disabled.

Fonts Tab

Manage fonts for your display:

Upload Fonts:

  • Drag and drop font files (.ttf, .otf, .bdf)
  • Upload multiple files at once
  • Progress indicator shows upload status

Font Catalog:

  • View all available fonts
  • See font previews
  • Check font sizes and styles

Font Preview:

  • Render sample text in any TTF/OTF font at a chosen size

Fonts used by a plugin are chosen in that plugin's own settings tab; the Fonts tab has no per-element override editor.

Delete Fonts:

  • Remove unused fonts
  • Free up disk space

Logs Tab

View real-time system logs:

Log Viewer:

  • Streaming logs from the display service
  • Auto-scroll to latest entries
  • Timestamps for each log entry

Filtering:

  • Filter by log level (INFO, WARNING, ERROR)
  • Search for specific text
  • Filter by plugin or component

Actions:

  • Refresh: Reload the log view
  • Clear: Clear the current view
  • Download: Download logs for offline analysis
  • Auto-scroll checkbox: toggle automatic scrolling to the latest entries

Common Tasks

Changing Display Brightness

  1. Open the Display tab
  2. Adjust the Brightness slider (1–100)
  3. Click Save. The panel picks up the new brightness within a few seconds; no restart is needed

Installing a New Plugin

  1. Open the Plugin Manager tab
  2. Scroll to the Plugin Store section and browse or search
  3. Click Install next to the plugin
  4. Toggle the plugin on in Installed Plugins. The running display loads it within a few seconds; no restart is needed

Configuring a Plugin

  1. Open the plugin's tab in the second nav row (each installed plugin has its own tab)
  2. Edit the auto-generated form
  3. Click Save
  4. Restart the display service from Overview

Setting Favorite Sports Teams

Sports favorites live in the relevant plugin's tab — there is no separate "Sports Configuration" tab. For example:

  1. Install Hockey Scoreboard from Plugin Manager → Plugin Store
  2. Open the Hockey Scoreboard tab in the second nav row
  3. Add your favorites under favorite_teams.<league> (e.g. favorite_teams.nhl)
  4. Click Save and restart the display service

Troubleshooting Display Issues

  1. Navigate to the Logs tab
  2. Look for ERROR or WARNING messages
  3. Filter by the problematic plugin or component
  4. Check the error message for clues
  5. See TROUBLESHOOTING.md for common solutions

Real-Time Features

The web interface uses Server-Sent Events (SSE) for real-time updates:

Live Updates:

  • System stats refresh automatically every few seconds
  • Display preview updates in real-time
  • Logs stream continuously
  • No page refresh required

Performance:

  • Minimal bandwidth usage
  • Server-side rendering for fast load times
  • The UI is built on Alpine.js and HTMX, so JavaScript must be enabled in the browser

Mobile Access

The interface is fully responsive and works on mobile devices:

Mobile Features:

  • Touch-friendly interface
  • Responsive layout adapts to screen size
  • All features available on mobile

Tips for Mobile:

  • Use landscape mode for better visibility
  • Pinch to zoom on display preview

API Access

The web interface is built on a REST API that you can access programmatically:

API Base URL:

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

The API blueprint (web_interface/blueprints/api_v3/) is registered at /api/v3 in web_interface/app.py.

Common Endpoints:

  • GET /api/v3/config/main — Get main configuration
  • POST /api/v3/config/main — Update main configuration
  • GET /api/v3/system/status — Get system status
  • POST /api/v3/system/action — Control display (start/stop/restart, reboot, etc.)
  • GET /api/v3/plugins/installed — List installed plugins
  • POST /api/v3/plugins/install — Install a plugin from the store
  • POST /api/v3/plugins/install-from-url — Install a plugin from a GitHub URL

Note: See REST_API_REFERENCE.md for complete API documentation.


Troubleshooting

Interface Won't Load

Problem: Browser shows "Unable to connect" or "Connection refused"

Solutions:

  1. Verify the web service is running:

    sudo systemctl status ledmatrix-web
    
  2. Start the service if stopped:

    sudo systemctl start ledmatrix-web
    
  3. Check that port 5000 is not blocked by firewall

  4. Verify the Pi's IP address is correct

Changes Not Applying

Problem: Configuration changes don't take effect

Solutions:

  1. Ensure you clicked "Save Configuration"
  2. Restart the display service for changes to apply:
    sudo systemctl restart ledmatrix
    
  3. Check logs for error messages

Display Preview Not Updating

Problem: Display preview shows old content or doesn't update

Solutions:

  1. Refresh the browser page (F5)
  2. Check that the display service is running
  3. Verify SSE streams are working (check browser console)

Plugin Configuration Not Saving

Problem: Plugin settings revert after restart

Solutions:

  1. Check file permissions on config/config.json:
    ls -l config/config.json
    
  2. Ensure the web service has write permissions
  3. Check logs for permission errors

Security Considerations

Network Access:

  • The interface is accessible to anyone on your local network
  • No authentication is currently implemented
  • Recommended for trusted networks only

Best Practices:

  1. Run on a private network (not exposed to internet)
  2. Use a firewall to restrict access if needed
  3. Consider VPN access for remote control
  4. Keep the system updated

Technical Details

Architecture

The web interface uses modern web technologies:

  • Backend: Flask with Blueprint-based modular design
  • Frontend: HTMX for dynamic content, Alpine.js for reactive components
  • Styling: Tailwind CSS for responsive design
  • Real-Time: Server-Sent Events (SSE) for live updates

File Locations

Configuration (relative to the LEDMatrix folder, e.g. ~/LEDMatrix):

  • Main config: config/config.json
  • Secrets: config/config_secrets.json
  • WiFi config: config/wifi_config.json

Logs:

  • Display service: sudo journalctl -u ledmatrix -f
  • Web service: sudo journalctl -u ledmatrix-web -f

Plugins:

  • Plugin directory: configurable via plugin_system.plugins_directory in config.json (default plugin-repos/). Main plugin discovery only scans this directory; the Plugin Store install flow and the schema loader additionally probe plugins/ so dev symlinks created by scripts/dev/dev_plugin_setup.sh keep working.
  • Plugin config: config/config.json (per-plugin sections)