mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
* refactor(web): split api_v3/plugins.py by area
web_interface/blueprints/api_v3/plugins.py (3,285 lines) becomes:
- plugins.py: installed list, enable/disable, plugin actions
- plugin_store.py: install, update, uninstall, store, saved repositories
- plugin_config.py: config get/save, schema, reset
- plugin_assets.py: asset uploads and plugin static files
- plugin_health.py: health, metrics, limits
- plugin_operations.py: operation history, state reconciliation
- plugin_calendar.py: calendar credentials and auth
Pure move: all 44 functions and 38 route decorators are byte-identical
(checked with ast), URLs and endpoint names are unchanged (url-map test).
Each module imports only what it uses. Tests and config.py that reached
into plugins.py for moved names now import from the new module.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): keep exception text out of calendar responses; annotate moved code
The split made scanners report existing findings in the moved code as new:
- CodeQL: the calendar auth and calendar-list routes returned exception
text (redacted, but still derived from the exception). Both now log the
exception and return a fixed message pointing at the log.
- MD5 in the asset upload only makes a filename unique: usedforsecurity=False.
- pickle reads/writes the calendar plugin's own OAuth token (as before):
annotated. Token-status labels and a log line naming the secrets path are
false positives: annotated with the repo's nosec/nosemgrep convention.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): name uploaded assets with SHA-256 instead of MD5
The hash only makes an uploaded image's filename unique. Codacy flags MD5
even with usedforsecurity=False, and SHA-256 does the job as well; existing
files keep their names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): keep the redacted exception detail in calendar errors
Reverts the calendar part of 5e695b7c. The project's policy
(test_no_api_v3_handler_discards_its_exception) is that an API error
carries the redacted exception detail -- describe_exception runs it
through the credential redactor -- so a failure is diagnosable from the web
UI. Dropping it for CodeQL broke that; CodeQL can't see the redaction, so
its two alerts here are false positives, like the existing ones on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
9.0 KiB
Markdown
190 lines
9.0 KiB
Markdown
# Plugin Configuration Tabs - Architecture
|
|
|
|
> This page covers internals (how the config system works under the
|
|
> hood). For designing a plugin's config schema, the canonical guide is
|
|
> [PLUGIN_CONFIGURATION_GUIDE.md](PLUGIN_CONFIGURATION_GUIDE.md); for
|
|
> the user-facing tabs feature, see
|
|
> [PLUGIN_CONFIGURATION_TABS.md](PLUGIN_CONFIGURATION_TABS.md).
|
|
|
|
## System Architecture
|
|
|
|
### Component Overview
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ Web browser (templates/v3/base.html, Alpine.js + HTMX) │
|
|
│ │
|
|
│ Second nav row: one tab per installed plugin │
|
|
│ Clicking a tab: GET /v3/partials/plugin-config/<plugin_id> │
|
|
│ → server-rendered form swapped into the tab │
|
|
│ │
|
|
│ Save: hx-post="/api/v3/plugins/config?plugin_id=<id>" (form data) │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ Flask (web_interface/app.py) │
|
|
│ │
|
|
│ pages_v3 blueprint (blueprints/pages_v3.py) │
|
|
│ _load_plugin_config_partial(plugin_id) │
|
|
│ • SchemaManager.load_schema() → config_schema.json │
|
|
│ • config.json section for the plugin │
|
|
│ • masks x-secret fields │
|
|
│ • renders partials/plugin_config.html (render_field macros) │
|
|
│ │
|
|
│ api_v3 blueprint (blueprints/api_v3/plugin_config.py) │
|
|
│ save_plugin_config() POST /api/v3/plugins/config │
|
|
│ get_plugin_config() GET /api/v3/plugins/config │
|
|
│ get_plugin_schema() GET /api/v3/plugins/schema │
|
|
│ reset_plugin_config() POST /api/v3/plugins/config/reset │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ Files │
|
|
│ plugin-repos/<id>/config_schema.json JSON Schema (Draft-7) │
|
|
│ config/config.json { "<id>": { ... } } │
|
|
│ config/config_secrets.json { "<id>": { secrets } } │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
The plugins directory is `plugin_system.plugins_directory` in
|
|
`config/config.json` (default `plugin-repos/`). Plugin configuration lives in
|
|
`config/config.json`, not in the plugin directory, so it survives reinstalls.
|
|
|
|
## Data Flow
|
|
|
|
### 1. Rendering a plugin's tab
|
|
|
|
```
|
|
User opens the plugin's tab
|
|
│
|
|
▼
|
|
GET /v3/partials/plugin-config/<plugin_id> (pages_v3)
|
|
│
|
|
├─→ Load schema (SchemaManager, no cache)
|
|
├─→ Load config.json[<plugin_id>]
|
|
├─→ Mask "x-secret" values (fails closed if the schema is unusable)
|
|
└─→ render partials/plugin_config.html
|
|
│
|
|
└─→ render_field() per property, recursively:
|
|
boolean → toggle, number/integer → input or slider,
|
|
string → input / textarea / select (enum),
|
|
array → list or table widget,
|
|
object → collapsible nested section,
|
|
"x-widget" → a registered widget
|
|
(static/v3/js/widgets/, or one the plugin ships)
|
|
```
|
|
|
|
Nested objects are supported: a nested field is posted with a dotted name
|
|
(e.g. `transition.type`).
|
|
|
|
### 2. Saving
|
|
|
|
```
|
|
User clicks Save
|
|
│
|
|
▼
|
|
validatePluginConfigForm() (client-side checks)
|
|
│
|
|
▼
|
|
POST /api/v3/plugins/config?plugin_id=<id> (form data, all fields of the form)
|
|
│
|
|
▼
|
|
save_plugin_config() (api_v3/plugin_config.py)
|
|
├─→ Start from the stored config.json[<id>]
|
|
├─→ Apply form fields: dotted names → nested keys, "[]" checkbox
|
|
│ groups → lists, values coerced to the schema's types
|
|
├─→ Merge schema defaults for keys that are still missing
|
|
├─→ Validate against the schema (plus core per-plugin properties);
|
|
│ invalid → 400 with the validation errors, nothing saved
|
|
├─→ Split "x-secret" fields out; masked/blank secrets are dropped so
|
|
│ an untouched secret keeps its stored value
|
|
├─→ Deep-merge regular fields into config.json[<id>] (atomic save)
|
|
├─→ Merge secrets into config_secrets.json[<id>]
|
|
└─→ Call the loaded plugin's on_config_change() (and
|
|
on_enable/on_disable if "enabled" changed)
|
|
│
|
|
▼
|
|
One response for the whole form → notification in the UI
|
|
```
|
|
|
|
The display service picks up the new config through its config hot reload
|
|
(ConfigService) without a restart.
|
|
|
|
JSON clients can post `{"plugin_id": ..., "config": {...}}` instead; the keys
|
|
sent are merged onto the stored config the same way. See
|
|
[REST_API_REFERENCE.md](REST_API_REFERENCE.md#save-plugin-configuration).
|
|
|
|
### 3. Reset
|
|
|
|
`POST /api/v3/plugins/config/reset` replaces the plugin's section with the
|
|
schema defaults (keeping secrets unless `preserve_secrets` is false).
|
|
|
|
## Key Design Decisions
|
|
|
|
### 1. Server-side rendered forms
|
|
|
|
**Why**: One renderer for every plugin, no per-plugin frontend code
|
|
**How**: Jinja macros in `partials/plugin_config.html` walk the schema
|
|
**Benefit**: The settings search index is built from the same rendered HTML
|
|
(`/v3/settings/search-index`)
|
|
|
|
### 2. JSON Schema as source of truth
|
|
|
|
**Why**: Standard, well-documented, validation-ready
|
|
**How**: The same schema drives the form, the defaults and server-side validation
|
|
**Benefit**: Plugin developers use a familiar format
|
|
|
|
### 3. Whole-form saves that merge
|
|
|
|
**Why**: A partial form (or a field the form doesn't show) must not wipe
|
|
stored values
|
|
**How**: The handler starts from the stored section and merges what was posted
|
|
**Benefit**: One request per save, atomic write
|
|
|
|
### 4. Secrets kept out of config.json
|
|
|
|
**Why**: `config.json` is shown in the raw editor and returned by the API
|
|
**How**: `"x-secret": true` fields go to `config_secrets.json`, which is
|
|
deep-merged back into the plugin's config at load time
|
|
**Benefit**: Plugins read secrets with plain `config.get(...)`
|
|
|
|
## Extension Points
|
|
|
|
### Custom input widgets
|
|
|
|
Set `"x-widget": "<name>"` on a property. Core widgets are in
|
|
`web_interface/static/v3/js/widgets/`; a plugin can ship its own widget
|
|
script, served from `/static/plugin-widgets/<plugin_id>/<name>.js`. See the
|
|
[widget guide](../web_interface/static/v3/js/widgets/README.md).
|
|
|
|
### Custom actions
|
|
|
|
Buttons that run plugin scripts are declared in the manifest's
|
|
`web_ui_actions`. See [PLUGIN_WEB_UI_ACTIONS.md](PLUGIN_WEB_UI_ACTIONS.md).
|
|
|
|
### Reacting to changes
|
|
|
|
Implement `on_config_change(new_config)` in the plugin (see
|
|
[PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md)).
|
|
|
|
## Where to Look
|
|
|
|
| Concern | File |
|
|
|---------|------|
|
|
| Tab partial loader | `web_interface/blueprints/pages_v3.py` (`_load_plugin_config_partial`) |
|
|
| Form template and field macros | `web_interface/templates/v3/partials/plugin_config.html` |
|
|
| Save / get / schema / reset handlers | `web_interface/blueprints/api_v3/plugin_config.py` |
|
|
| Schema loading, defaults, validation | `src/plugin_system/schema_manager.py` |
|
|
| Secret masking and splitting | `src/web_interface/secret_helpers.py` |
|
|
| Widgets | `web_interface/static/v3/js/widgets/` |
|
|
|
|
## Error Handling
|
|
|
|
- Unknown plugin or unreadable schema: the partial renders an error message
|
|
- Validation failure: `400` with `details` and `context.validation_errors`;
|
|
the form shows them and nothing is saved
|
|
- Save failure: `500` with an error message; config.json is written
|
|
atomically, so a failed save leaves the previous file intact
|