Files
LEDMatrix/docs/PLUGIN_CUSTOM_ICONS_FEATURE.md
Chuck a62d4529fb docs: flag aspirational/regressed features in plugin docs
These docs describe features that exist as documented in the doc but
either never wired up or regressed when v3 shipped. Each gets a clear
status banner so plugin authors don't waste time chasing features that
don't actually work.

FONT_MANAGER.md
- The "For Plugin Developers / Plugin Font Registration" section
  documents adding a "fonts" block to manifest.json that gets
  registered via FontManager.register_plugin_fonts(). The method
  exists at src/font_manager.py:150 but is **never called from
  anywhere** in the codebase (verified: zero callers). A plugin
  shipping a manifest "fonts" block has its fonts silently ignored.
  Added a status warning and a note about how to actually ship plugin
  fonts (regular files in the plugin dir, loaded directly).

PLUGIN_IMPLEMENTATION_SUMMARY.md
- Added a top-level status banner.
- Architecture diagram referenced src/plugin_system/registry_manager.py
  (which doesn't exist) and listed plugins/ as the install location.
  Replaced with the real file list (plugin_loader, schema_manager,
  health_monitor, operation_queue, state_manager) and pointed at
  plugin-repos/ as the default install location.
- "Dependency Management: Virtual Environments" — verified there's no
  per-plugin venv. Removed the bullet and added a note that plugin
  Python deps install into the system Python environment, with no
  conflict resolution.
- "Permission System: File Access Control / Network Access /
  Resource Limits / CPU and memory constraints" — none of these
  exist. There's a resource_monitor.py and health_monitor.py for
  metrics/warnings, but no hard caps or sandboxing. Replaced the
  section with what's actually implemented and a clear note that
  plugins run in the same process with full file/network access.

PLUGIN_CUSTOM_ICONS.md and PLUGIN_CUSTOM_ICONS_FEATURE.md
- The custom-icon feature was implemented in the v2 web interface
  via a getPluginIcon() helper in templates/index_v2.html that read
  the manifest "icon" field. When the v3 web interface was built,
  that helper wasn't ported. Verified in
  web_interface/templates/v3/base.html:515 and :774, plugin tab
  icons are hardcoded to `fas fa-puzzle-piece`. The "icon" field in
  plugin manifests is currently silently ignored (verified with grep
  across web_interface/ and src/plugin_system/ — zero non-action-
  related reads of plugin.icon or manifest.icon).
- Added a status banner to both docs noting the regression so plugin
  authors don't think their custom icons are broken in their own
  plugin code.

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

11 KiB

Plugin Custom Icons Feature

⚠️ Status: this doc describes the v2 web interface implementation of plugin custom icons. The feature regressed when the v3 web interface was built — the getPluginIcon() helper referenced below lived in templates/index_v2.html (which is now archived) and was not ported to the v3 templates. Plugin tab icons in v3 are hardcoded to fas fa-puzzle-piece (web_interface/templates/v3/base.html:515 and :774). The icon field in manifest.json is currently silently ignored.

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! 🚀