Files
LEDMatrix/docs/PLUGIN_CUSTOM_ICONS_FEATURE.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

11 KiB

Plugin Custom Icons Feature - Complete

What Was Implemented

You asked: "How could a plugin add their own custom icon?"

Answer: Plugins can now specify custom icons in their manifest.json file using the icon field!

Features Delivered

Font Awesome Support - Use any Font Awesome icon (e.g., fas fa-clock)
Emoji Support - Use any emoji character (e.g., or 👋)
Custom Image Support - Use custom image files or URLs
Automatic Detection - System automatically detects icon type
Fallback Support - Default puzzle piece icon if none specified
Tab & Header Icons - Icons appear in both tab buttons and configuration page headers

How It Works

For Plugin Developers

Simply add an icon field to your plugin's manifest.json:

{
  "id": "my-plugin",
  "name": "My Plugin",
  "icon": "fas fa-star",  // ← Add this line
  "config_schema": "config_schema.json",
  ...
}

Three Icon Types Supported

"icon": "fas fa-clock"

Best for: Professional, consistent UI appearance

2. Emoji Icons (Fun!)

"icon": "⏰"

Best for: Colorful, fun plugins; no setup needed

3. Custom Images

"icon": "/plugins/my-plugin/logo.png"

Best for: Unique branding; requires image file

Implementation Details

Frontend Changes (templates/index_v2.html)

New Function: getPluginIcon(plugin)

  • Checks if plugin has icon field in manifest
  • Detects icon type automatically:
    • Contains fa- → Font Awesome
    • 1-4 characters → Emoji
    • Starts with URL/path → Custom image
    • Otherwise → Default puzzle piece

Updated Functions:

  • generatePluginTabs() - Uses custom icon for tab button
  • generatePluginConfigForm() - Uses custom icon in page header

Example Plugin Updates

hello-world plugin:

"icon": "👋"

clock-simple plugin:

"icon": "fas fa-clock"

Code Example

Here's what the icon detection logic does. Important: Plugin manifests must be treated as untrusted input and require escaping/validation before rendering.

// Helper function to escape HTML entities
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Helper function to validate and sanitize image URLs
function isValidImageUrl(url) {
    if (!url || typeof url !== 'string') {
        return false;
    }
    
    // Only allow http, https, or relative paths starting with /
    const allowedProtocols = ['http:', 'https:'];
    const urlLower = url.toLowerCase().trim();
    
    // Reject dangerous protocols
    if (urlLower.startsWith('javascript:') || 
        urlLower.startsWith('data:') || 
        urlLower.startsWith('vbscript:') ||
        urlLower.startsWith('onerror=') ||
        urlLower.startsWith('onload=')) {
        return false;
    }
    
    // Allow relative paths starting with /
    if (url.startsWith('/')) {
        return true;
    }
    
    // Validate absolute URLs
    try {
        const urlObj = new URL(url);
        return allowedProtocols.includes(urlObj.protocol);
    } catch (e) {
        // Invalid URL format
        return false;
    }
}

// Helper function to safely validate Font Awesome class names
function isValidFontAwesomeClass(icon) {
    // Whitelist pattern: only allow alphanumeric, dash, underscore, and spaces
    // Must contain 'fa-' for Font Awesome
    const faPattern = /^[a-zA-Z0-9\s_-]*fa-[a-zA-Z0-9-]+[a-zA-Z0-9\s_-]*$/;
    return faPattern.test(icon) && icon.includes('fa-');
}

function getPluginIcon(plugin) {
    if (plugin.icon) {
        const icon = String(plugin.icon).trim();
        
        // Font Awesome icon - escape class name to prevent XSS
        if (isValidFontAwesomeClass(icon)) {
            const escapedIcon = escapeHtml(icon);
            return `<i class="${escapedIcon}"></i>`;
        }
        
        // Emoji - use textContent to safely render (no HTML injection possible)
        if (icon.length <= 4) {
            // Create element and set textContent (safe from XSS)
            const span = document.createElement('span');
            span.style.fontSize = '1.1em';
            span.textContent = icon; // textContent automatically escapes
            return span.outerHTML;
        }
        
        // Custom image - validate URL and set src attribute safely
        if (isValidImageUrl(icon)) {
            // Create img element and set attributes safely
            const img = document.createElement('img');
            img.src = icon; // URL already validated
            img.alt = '';
            img.style.width = '16px';
            img.style.height = '16px';
            return img.outerHTML;
        }
    }
    
    // Default fallback
    return '<i class="fas fa-puzzle-piece"></i>';
}

Security Notes:

  • Plugin manifests are treated as untrusted input
  • All text content is escaped using escapeHtml() or textContent
  • Image URLs are validated to only allow http://, https://, or relative paths starting with /
  • Dangerous protocols (javascript:, data:, etc.) are explicitly rejected
  • Font Awesome class names are validated against a whitelist pattern
  • DOM elements are created and attributes set directly rather than using string interpolation

Visual Examples

Before (No Custom Icons)

[🧩 Hello World] [🧩 Clock Simple] [🧩 Weather Display]

After (With Custom Icons)

[👋 Hello World] [⏰ Clock Simple] [☀️ Weather Display]

Documentation Created

📚 Comprehensive guide: docs/PLUGIN_CUSTOM_ICONS.md

Contains:

  • Complete icon type explanations
  • Font Awesome icon recommendations by category
  • Emoji suggestions for common plugin types
  • Custom image guidelines
  • Best practices and troubleshooting
  • Examples for every use case

📝 Updated existing docs:

  • PLUGIN_CONFIGURATION_TABS.md - Added icon reference
  • PLUGIN_CONFIG_TABS_SUMMARY.md - Added icon quick tip
  • PLUGIN_CONFIG_QUICK_START.md - Added icon bonus section

By Plugin Category

Time & Calendar

  • Font Awesome: fas fa-clock, fas fa-calendar, fas fa-hourglass
  • Emoji: 📅 ⏱️

Weather

  • Font Awesome: fas fa-cloud-sun, fas fa-temperature-high
  • Emoji: ☀️ 🌧️ ⛈️

Finance

  • Font Awesome: fas fa-chart-line, fas fa-dollar-sign
  • Emoji: 💰 📈 💵

Sports

  • Font Awesome: fas fa-football-ball, fas fa-trophy
  • Emoji: 🏀 🎮

Music

  • Font Awesome: fas fa-music, fas fa-headphones
  • Emoji: 🎵 🎶 🎸

News

  • Font Awesome: fas fa-newspaper, fas fa-rss
  • Emoji: 📰 📡 📻

Utilities

  • Font Awesome: fas fa-tools, fas fa-cog
  • Emoji: 🔧 ⚙️ 🛠️

Usage Examples

Weather Plugin

{
  "id": "weather-pro",
  "name": "Weather Pro",
  "icon": "fas fa-cloud-sun",
  "description": "Advanced weather display"
}

Result: ☁️ Weather Pro tab

Game Scores

{
  "id": "game-scores",
  "name": "Game Scores",
  "icon": "🎮",
  "description": "Live game scores"
}

Result: 🎮 Game Scores tab

Custom Branding

{
  "id": "company-metrics",
  "name": "Company Metrics",
  "icon": "/plugins/company-metrics/logo.svg",
  "description": "Internal dashboard"
}

Result: [logo] Company Metrics tab

Benefits

For Users

  • Visual Recognition - Instantly identify plugins
  • Better Navigation - Find plugins faster
  • Professional Appearance - Polished, modern UI

For Developers

  • Easy to Add - Just one line in manifest
  • Flexible Options - Choose what fits your plugin
  • No Code Required - Pure configuration

For the Project

  • Plugin Differentiation - Each plugin stands out
  • Enhanced UX - More intuitive interface
  • Branding Support - Plugins can show identity

Backward Compatibility

Fully backward compatible

  • Plugins without icon field still work
  • Default puzzle piece icon used automatically
  • No breaking changes to existing plugins

Testing

To test custom icons:

  1. Open web interface at http://your-pi-ip:5000
  2. Check installed plugins:
    • Hello World should show 👋
    • Clock Simple should show 🕐
  3. Install a new plugin with custom icon
  4. Verify icon appears in:
    • Tab navigation bar
    • Plugin configuration page header

File Changes

Modified Files

  • templates/index_v2.html
    • Added getPluginIcon() function
    • Updated generatePluginTabs()
    • Updated generatePluginConfigForm()

Updated Plugin Manifests

  • ledmatrix-plugins/plugins/hello-world/manifest.json - Added emoji icon
  • ledmatrix-plugins/plugins/clock-simple/manifest.json - Added Font Awesome icon

New Documentation

  • docs/PLUGIN_CUSTOM_ICONS.md - Complete guide (80+ lines)

Updated Documentation

  • docs/PLUGIN_CONFIGURATION_TABS.md
  • docs/PLUGIN_CONFIG_TABS_SUMMARY.md
  • docs/PLUGIN_CONFIG_QUICK_START.md

Quick Reference

Add Icon to Your Plugin

{
  "id": "your-plugin",
  "name": "Your Plugin Name",
  "icon": "fas fa-star",  // or emoji or image URL
  "config_schema": "config_schema.json",
  ...
}

Icon Format Examples

// Font Awesome
"icon": "fas fa-star"
"icon": "far fa-heart"
"icon": "fab fa-twitter"

// Emoji
"icon": "⭐"
"icon": "❤️"
"icon": "🐦"

// Custom Image
"icon": "/plugins/my-plugin/icon.png"
"icon": "https://example.com/logo.svg"

Browse Available Icons

Best Practices

  1. Choose meaningful icons - Icon should relate to plugin function
  2. Keep it simple - Works better at small sizes
  3. Test visibility - Ensure icon is clear at 16px
  4. Match UI style - Font Awesome recommended for consistency
  5. Document choice - Note icon meaning in plugin README

Troubleshooting

Icon not showing?

  • Check manifest syntax (JSON valid?)
  • Verify icon field spelling
  • Refresh plugins in web interface
  • Check browser console for errors

Wrong icon appearing?

  • Font Awesome: Verify class name at fontawesome.com
  • Emoji: Try different emoji (platform rendering varies)
  • Custom image: Check file path and permissions

Future Enhancements

Possible future improvements:

  • Icon picker in plugin store
  • Animated icons support
  • SVG path support
  • Icon themes/styles
  • Dynamic icon changes based on state

Summary

Mission accomplished! 🎉

Plugins can now have custom icons by adding one line to their manifest:

"icon": "fas fa-your-icon"

Three formats supported:

  • Font Awesome (professional)
  • Emoji (fun)
  • Custom images (branded)

The feature is:

  • Easy to use (one line)
  • Flexible (three options)
  • Backward compatible
  • Well documented
  • Already working in example plugins

Ready to use! 🚀