Files
LEDMatrix/docs/PLUGIN_CONFIG_ARCHITECTURE.md
Chuck 2f3433cebc docs: fix misc remaining docs (architecture, dev quickref, sub-dir READMEs)
PLUGIN_ARCHITECTURE_SPEC.md
- Added a banner at the top noting this is a historical design doc
  written before the plugin system shipped. The doc is ~1900 lines
  with 13 stale /api/plugins/* paths (real is /api/v3/plugins/*),
  references to web_interface_v2.py (current is app.py), and a
  Migration Strategy / Implementation Roadmap that's now history.
  Banner points readers at the current docs
  (PLUGIN_DEVELOPMENT_GUIDE, PLUGIN_API_REFERENCE,
  REST_API_REFERENCE) without needing to retrofit every section.

PLUGIN_CONFIG_ARCHITECTURE.md
- 10 occurrences of /api/plugins/* missing /v3 prefix. Bulk fixed.

DEVELOPER_QUICK_REFERENCE.md
- cache_manager.delete("key") -> cache_manager.clear_cache("key")
  with comment noting delete() doesn't exist. Same bug already
  documented in PLUGIN_API_REFERENCE.md.

SSH_UNAVAILABLE_AFTER_INSTALL.md
- 4 occurrences of port 5001 -> 5000 in AP-mode and Ethernet/WiFi
  recovery instructions.

PLUGIN_CUSTOM_ICONS_FEATURE.md
- Port 5001 -> 5000.

CONFIG_DEBUGGING.md
- Documented /api/v3/config/plugin/<id> and /api/v3/config/validate
  endpoints don't exist. Replaced with the real endpoints:
  /api/v3/config/main, /api/v3/plugins/schema?plugin_id=,
  /api/v3/plugins/config?plugin_id=. Added a note that validation
  runs server-side automatically on POST.

STARLARK_APPS_GUIDE.md
- "Plugins -> Starlark Apps" UI navigation path doesn't exist (5
  occurrences). Replaced with the real path: Plugin Manager tab,
  then the per-plugin Starlark Apps tab in the second nav row.
- "Navigate to Plugins" install step -> Plugin Manager tab.

web_interface/README.md
- Documented several endpoints that don't exist in the api_v3
  blueprint:
  - GET /api/v3/plugins (list) -> /api/v3/plugins/installed
  - GET /api/v3/plugins/<id> -> doesn't exist
  - POST /api/v3/plugins/<id>/config -> POST /api/v3/plugins/config
  - GET /api/v3/plugins/<id>/enable + /disable -> POST /api/v3/plugins/toggle
  - GET /api/v3/store/plugins -> /api/v3/plugins/store/list
  - POST /api/v3/store/install/<id> -> POST /api/v3/plugins/install
  - POST /api/v3/store/uninstall/<id> -> POST /api/v3/plugins/uninstall
  - POST /api/v3/store/update/<id> -> POST /api/v3/plugins/update
  - POST /api/v3/display/start/stop/restart -> POST /api/v3/system/action
  - GET /api/v3/display/status -> GET /api/v3/system/status
- Also fixed config/secrets.json -> config/config_secrets.json
- Replaced the per-section endpoint duplication with a current real
  endpoint list and a pointer to docs/REST_API_REFERENCE.md.
- Documented that SSE stream endpoints are defined directly on the
  Flask app at app.py:607-615, not in the api_v3 blueprint.

scripts/install/README.md
- Was missing 3 of the 9 install scripts in the directory:
  one-shot-install.sh, configure_wifi_permissions.sh, and
  debug_install.sh. Added them with brief descriptions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:19:47 -04:00

16 KiB

Plugin Configuration Tabs - Architecture

System Architecture

Component Overview

┌─────────────────────────────────────────────────────────────────┐
│                         Web Browser                              │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    Tab Navigation Bar                     │   │
│  │  [Overview] [General] ... [Plugins] [Plugin X] [Plugin Y]│   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                   │
│  ┌─────────────────┐  ┌──────────────────────────────────┐     │
│  │  Plugins Tab    │  │   Plugin X Configuration Tab     │     │
│  │                 │  │                                    │     │
│  │ • Install       │  │  Form Generated from Schema:      │     │
│  │ • Update        │  │  • Boolean → Toggle               │     │
│  │ • Uninstall     │  │  • Number → Number Input          │     │
│  │ • Enable        │  │  • String → Text Input            │     │
│  │ • [Configure]──────→  • Array → Comma Input           │     │
│  │                 │  │  • Enum → Dropdown                │     │
│  └─────────────────┘  │                                    │     │
│                        │  [Save] [Back] [Reset]            │     │
│                        └──────────────────────────────────┘     │
└─────────────────────────────────────────────────────────────────┘
                                │
                                │ HTTP API
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Flask Backend                               │
│  ┌───────────────────────────────────────────────────────┐     │
│  │         /api/v3/plugins/installed                         │     │
│  │  • Discover plugins in plugins/ directory              │     │
│  │  • Load manifest.json for each plugin                  │     │
│  │  • Load config_schema.json if exists                   │     │
│  │  • Load current config from config.json                │     │
│  │  • Return combined data to frontend                    │     │
│  └───────────────────────────────────────────────────────┘     │
│                                                                   │
│  ┌───────────────────────────────────────────────────────┐     │
│  │         /api/v3/plugins/config                            │     │
│  │  • Receive key-value pair                              │     │
│  │  • Update config.json                                  │     │
│  │  • Return success/error                                │     │
│  └───────────────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────────────┘
                                │
                                │ File System
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                      File System                                 │
│                                                                   │
│  plugins/                                                         │
│  ├── hello-world/                                                │
│  │   ├── manifest.json ───┐                                     │
│  │   ├── config_schema.json ─┼─→ Defines UI structure           │
│  │   ├── manager.py          │                                   │
│  │   └── requirements.txt    │                                   │
│  └── clock-simple/            │                                   │
│      ├── manifest.json        │                                   │
│      └── config_schema.json ──┘                                  │
│                                                                   │
│  config/                                                          │
│  └── config.json ────────────→ Stores configuration values       │
│      {                                                            │
│        "hello-world": {                                           │
│          "enabled": true,                                         │
│          "message": "Hello!",                                     │
│          ...                                                      │
│        }                                                          │
│      }                                                            │
└─────────────────────────────────────────────────────────────────┘

Data Flow

1. Page Load Sequence

User Opens Web Interface
         │
         ▼
DOMContentLoaded Event
         │
         ▼
refreshPlugins()
         │
         ▼
GET /api/v3/plugins/installed
         │
         ├─→ For each plugin directory:
         │   ├─→ Read manifest.json
         │   ├─→ Read config_schema.json (if exists)
         │   └─→ Read config from config.json
         │
         ▼
Return JSON Array:
[{
  id: "hello-world",
  name: "Hello World",
  config: { enabled: true, message: "Hello!" },
  config_schema_data: {
    properties: {
      enabled: { type: "boolean", ... },
      message: { type: "string", ... }
    }
  }
}, ...]
         │
         ▼
generatePluginTabs(plugins)
         │
         ├─→ For each plugin:
         │   ├─→ Create tab button
         │   ├─→ Create tab content div
         │   └─→ generatePluginConfigForm(plugin)
         │           │
         │           ├─→ Read schema properties
         │           ├─→ Get current config values
         │           └─→ Generate HTML form inputs
         │
         ▼
Tabs Rendered in UI

2. Configuration Save Sequence

User Modifies Form
         │
         ▼
User Clicks "Save"
         │
         ▼
savePluginConfiguration(pluginId)
         │
         ├─→ Get form data
         ├─→ For each field:
         │   ├─→ Get schema type
         │   ├─→ Convert value to correct type
         │   │   • boolean: checkbox.checked
         │   │   • integer: parseInt()
         │   │   • number: parseFloat()
         │   │   • array: split(',')
         │   │   • string: as-is
         │   │
         │   └─→ POST /api/v3/plugins/config
         │       {
         │         plugin_id: "hello-world",
         │         key: "message",
         │         value: "Hello, World!"
         │       }
         │
         ▼
Backend Updates config.json
         │
         ▼
Return Success
         │
         ▼
Show Notification
         │
         ▼
Refresh Plugins

Class and Function Hierarchy

Frontend (JavaScript)

Window Load
  └── DOMContentLoaded
      └── refreshPlugins()
          ├── fetch('/api/v3/plugins/installed')
          ├── renderInstalledPlugins(plugins)
          └── generatePluginTabs(plugins)
              └── For each plugin:
                  ├── Create tab button
                  ├── Create tab content
                  └── generatePluginConfigForm(plugin)
                      ├── Read config_schema_data
                      ├── Read current config
                      └── Generate form HTML
                          ├── Boolean → Toggle switch
                          ├── Number → Number input
                          ├── String → Text input
                          ├── Array → Comma-separated input
                          └── Enum → Select dropdown

User Interactions
  ├── configurePlugin(pluginId)
  │   └── showTab(`plugin-${pluginId}`)
  │
  ├── savePluginConfiguration(pluginId)
  │   ├── Process form data
  │   ├── Convert types per schema
  │   └── For each field:
  │       └── POST /api/v3/plugins/config
  │
  └── resetPluginConfig(pluginId)
      ├── Get schema defaults
      └── For each field:
          └── POST /api/v3/plugins/config

Backend (Python)

Flask Routes
  ├── /api/v3/plugins/installed (GET)
  │   └── api_plugins_installed()
  │       ├── PluginManager.discover_plugins()
  │       ├── For each plugin:
  │       │   ├── PluginManager.get_plugin_info()
  │       │   ├── Load config_schema.json
  │       │   └── Load config from config.json
  │       └── Return JSON response
  │
  └── /api/v3/plugins/config (POST)
      └── api_plugin_config()
          ├── Parse request JSON
          ├── Load current config
          ├── Update config[plugin_id][key] = value
          └── Save config.json

File Structure

LEDMatrix/
│
├── web_interface_v2.py
│   └── Flask backend with plugin API endpoints
│
├── templates/
│   └── index_v2.html
│       └── Frontend with dynamic tab generation
│
├── config/
│   └── config.json
│       └── Stores all plugin configurations
│
├── plugins/
│   ├── hello-world/
│   │   ├── manifest.json           ← Plugin metadata
│   │   ├── config_schema.json      ← UI schema definition
│   │   ├── manager.py              ← Plugin logic
│   │   └── requirements.txt
│   │
│   └── clock-simple/
│       ├── manifest.json
│       ├── config_schema.json
│       └── manager.py
│
└── docs/
    ├── PLUGIN_CONFIGURATION_TABS.md       ← Full documentation
    ├── PLUGIN_CONFIG_TABS_SUMMARY.md      ← Implementation summary
    ├── PLUGIN_CONFIG_QUICK_START.md       ← Quick start guide
    └── PLUGIN_CONFIG_ARCHITECTURE.md      ← This file

Key Design Decisions

1. Dynamic Tab Generation

Why: Plugins are installed/uninstalled dynamically
How: JavaScript creates/removes tab elements on plugin list refresh
Benefit: No server-side template rendering needed

2. JSON Schema as Source of Truth

Why: Standard, well-documented, validation-ready
How: Frontend interprets schema to generate forms
Benefit: Plugin developers use familiar format

3. Individual Config Updates

Why: Simplifies backend API
How: Each field saved separately via /api/v3/plugins/config
Benefit: Atomic updates, easier error handling

4. Type Conversion in Frontend

Why: HTML forms only return strings
How: JavaScript converts based on schema type before sending
Benefit: Backend receives correctly-typed values

5. No Nested Objects

Why: Keeps UI simple
How: Only flat property structures supported
Benefit: Easy form generation, clear to users

Extension Points

Adding New Input Types

Location: generatePluginConfigForm() in index_v2.html

if (type === 'your-new-type') {
    formHTML += `
        <!-- Your custom input HTML -->
    `;
}

Custom Validation

Location: savePluginConfiguration() in index_v2.html

// Add validation before sending
if (!validateCustomConstraint(value, propSchema)) {
    throw new Error('Validation failed');
}

Backend Hook

Location: api_plugin_config() in web_interface_v2.py

# Add custom logic before saving
if plugin_id == 'special-plugin':
    value = transform_value(value)

Performance Considerations

Frontend

  • Tab Generation: O(n) where n = number of plugins (typically < 20)
  • Form Generation: O(m) where m = number of config properties (typically < 10)
  • Memory: Each plugin tab ~5KB HTML
  • Total Impact: Negligible for typical use cases

Backend

  • Schema Loading: Cached after first load
  • Config Updates: Single file write (atomic)
  • API Calls: One per config field on save (sequential)
  • Optimization: Could batch updates in single API call

Security Considerations

  1. Input Validation: Schema constraints enforced client-side (UX) and should be enforced server-side
  2. Path Traversal: Plugin paths validated against known plugin directory
  3. XSS: All user inputs escaped before rendering in HTML
  4. CSRF: Flask CSRF tokens should be used in production
  5. File Permissions: config.json requires write access

Error Handling

Frontend

  • Network errors: Show notification, don't crash
  • Schema errors: Graceful fallback to no config tab
  • Type errors: Log to console, continue processing other fields

Backend

  • Invalid plugin_id: 400 Bad Request
  • Schema not found: Return null, frontend handles gracefully
  • Config save error: 500 Internal Server Error with message

Testing Strategy

Unit Tests

  • generatePluginConfigForm() for each schema type
  • Type conversion logic in savePluginConfiguration()
  • Backend schema loading logic

Integration Tests

  • Full save flow: form → API → config.json
  • Tab generation from API response
  • Reset to defaults

E2E Tests

  • Install plugin → verify tab appears
  • Configure plugin → verify config saved
  • Uninstall plugin → verify tab removed

Monitoring

Frontend Metrics

  • Time to generate tabs
  • Form submission success rate
  • User interactions (configure, save, reset)

Backend Metrics

  • API response times
  • Config update success rate
  • Schema loading errors

User Feedback

  • Are users finding the configuration interface?
  • Are validation errors clear?
  • Are default values sensible?

Future Roadmap

Phase 2: Enhanced Validation

  • Real-time validation feedback
  • Custom error messages
  • Dependent field validation

Phase 3: Advanced Inputs

  • Color pickers for RGB arrays
  • File upload for assets
  • Rich text editor for descriptions

Phase 4: Configuration Management

  • Export/import configurations
  • Configuration presets
  • Version history/rollback

Phase 5: Developer Tools

  • Schema editor in web UI
  • Live preview while editing schema
  • Validation tester