mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
* fix: post-audit follow-up code fixes (cache, fonts, icons, dev script, CI) The docs refresh effort (#306, ledmatrix-plugins#92) surfaced seven code bugs that were intentionally left out of the docs PRs because they required code changes rather than doc fixes. This PR addresses the six that belong in LEDMatrix (the seventh — a lacrosse-scoreboard mode rename — lives in the plugins repo). Bug 1: cache_manager.delete() AttributeError src/common/api_helper.py:287 and src/plugin_system/resource_monitor.py:343 both call cache_manager.delete(key), which doesn't exist — only clear_cache(key=None). Added a delete() alias method on CacheManager that forwards to clear_cache(key). Reverts the "There is no delete() method" wording in DEVELOPER_QUICK_REFERENCE, .cursorrules so the docs match the new shim. Bug 2: dev_plugin_setup.sh PROJECT_ROOT resolution scripts/dev/dev_plugin_setup.sh:9 set PROJECT_ROOT to SCRIPT_DIR instead of walking up two levels to the repo root, so PLUGINS_DIR resolved to scripts/dev/plugins/ and created symlinks under the script's own directory. Fixed the path and removed the stray scripts/dev/plugins/of-the-day symlink left by earlier runs. Bug 3: plugin custom icons regressed from v2 to v3 web_interface/blueprints/api_v3.py built the /plugins/installed response without including the manifest's "icon" field, and web_interface/templates/v3/base.html hardcoded fas fa-puzzle-piece in all three plugin-tab render sites. Pass the icon through the API and read it from the templates with a puzzle-piece fallback. Reverts the "currently broken" banners in docs/PLUGIN_CUSTOM_ICONS.md and docs/PLUGIN_CUSTOM_ICONS_FEATURE.md. Bug 4: register_plugin_fonts was never wired up src/font_manager.py:150 defines register_plugin_fonts(plugin_id, font_manifest) but nothing called it, so plugin manifests with a "fonts" block were silently no-ops. Wired the call into PluginManager.load_plugin() right after plugin_loader.load_plugin returns. Reverts the "not currently wired" warning in docs/FONT_MANAGER.md's "For Plugin Developers" section. Bug 5: dead web_interface_v2 import pattern (LEDMatrix half) src/base_odds_manager.py had a try/except importing web_interface_v2.increment_api_counter, falling back to a no-op stub. The module doesn't exist anywhere in the v3 codebase and no API metrics dashboard reads it. Deleted the import block and the single call site; the plugins-repo half of this cleanup lands in ledmatrix-plugins#<next>. Bug 7: no CI test workflow .github/workflows/ only contained security-audit.yml; pytest ran locally but was not gated on PRs. Added .github/workflows/tests.yml running pytest against Python 3.10, 3.11, 3.12 in EMULATOR=true mode, skipping tests marked hardware or slow. Updated docs/HOW_TO_RUN_TESTS.md to reflect that the workflow now exists. Verification done locally: - CacheManager.delete(key) round-trips with set/get - base_odds_manager imports without the v2 module present - dev_plugin_setup.sh PROJECT_ROOT resolves to repo root - api_v3 and plugin_manager compile clean - tests.yml YAML parses - Script syntax check on dev_plugin_setup.sh Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review comments on #307 - src/cache_manager.py: clear_cache(key) treated empty string as "wipe all" because of `if key:`. Switched to `key is None` branching, made delete(key) and clear_cache(key) reject empty strings and None outright with ValueError, and updated both docstrings to make the contract explicit. Verified locally with a round-trip test that clear_cache() (no arg) still wipes everything but clear_cache("") and delete("") raise. - src/plugin_system/plugin_manager.py: was reaching for the font manager via getattr(self.display_manager, 'font_manager', None). PluginManager already takes a dedicated font_manager parameter (line 54) and stores it as self.font_manager (line 69), so the old path was both wrong and could miss the font manager entirely when the host injects them separately. Switched to self.font_manager directly with the same try/except warning behavior. - web_interface/templates/v3/base.html: in the full plugin-tab renderer, the icon was injected with `<i class="${escapeHtml(plugin.icon)}">` — but escapeHtml only escapes <, >, and &, not double quotes, so a manifest with a quote in its icon string could break out of the class attribute. Replaced the innerHTML template with createElement for the <i> tag, set className from plugin.icon directly (no string interpolation), and used a text node for the label. Same fix shape would also harden the two stub-renderer sites at line 515 / 774, but those already escape `"` to " and CodeRabbit only flagged this site, so leaving them for now. - docs/FONT_MANAGER.md: clarified that the Manual Font Overrides *workflow* (set_override / remove_override / font_overrides.json) is the supported override path today, and only the Fonts tab in the web UI is the placeholder. Previous wording conflated the two and made it sound like overrides themselves were broken. - docs/HOW_TO_RUN_TESTS.md: replaced the vague "see the PR adding it" with a concrete link to #307 and a note that the workflow file itself is held back pending the workflow scope. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
435 lines
11 KiB
Markdown
435 lines
11 KiB
Markdown
# Plugin Custom Icons Feature
|
|
|
|
> **Note:** this doc was originally written against the v2 web
|
|
> interface. The v3 web interface now honors the same `icon` field
|
|
> in `manifest.json` — the API passes it through at
|
|
> `web_interface/blueprints/api_v3.py` and the three plugin-tab
|
|
> render sites in `web_interface/templates/v3/base.html` read it
|
|
> with a `fas fa-puzzle-piece` fallback. The guidance below still
|
|
> applies; only the referenced template/helper names differ.
|
|
|
|
## 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`:
|
|
|
|
```json
|
|
{
|
|
"id": "my-plugin",
|
|
"name": "My Plugin",
|
|
"icon": "fas fa-star", // ← Add this line
|
|
"config_schema": "config_schema.json",
|
|
...
|
|
}
|
|
```
|
|
|
|
### Three Icon Types Supported
|
|
|
|
#### 1. Font Awesome Icons (Recommended)
|
|
```json
|
|
"icon": "fas fa-clock"
|
|
```
|
|
|
|
Best for: Professional, consistent UI appearance
|
|
|
|
#### 2. Emoji Icons (Fun!)
|
|
```json
|
|
"icon": "⏰"
|
|
```
|
|
|
|
Best for: Colorful, fun plugins; no setup needed
|
|
|
|
#### 3. Custom Images
|
|
```json
|
|
"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:**
|
|
```json
|
|
"icon": "👋"
|
|
```
|
|
|
|
**clock-simple plugin:**
|
|
```json
|
|
"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.
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
## Popular Icon Recommendations
|
|
|
|
### 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
|
|
```json
|
|
{
|
|
"id": "weather-pro",
|
|
"name": "Weather Pro",
|
|
"icon": "fas fa-cloud-sun",
|
|
"description": "Advanced weather display"
|
|
}
|
|
```
|
|
Result: `☁️ Weather Pro` tab
|
|
|
|
### Game Scores
|
|
```json
|
|
{
|
|
"id": "game-scores",
|
|
"name": "Game Scores",
|
|
"icon": "🎮",
|
|
"description": "Live game scores"
|
|
}
|
|
```
|
|
Result: `🎮 Game Scores` tab
|
|
|
|
### Custom Branding
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"id": "your-plugin",
|
|
"name": "Your Plugin Name",
|
|
"icon": "fas fa-star", // or emoji or image URL
|
|
"config_schema": "config_schema.json",
|
|
...
|
|
}
|
|
```
|
|
|
|
### Icon Format Examples
|
|
|
|
```json
|
|
// 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
|
|
|
|
- **Font Awesome:** [fontawesome.com/icons](https://fontawesome.com/icons) (Free tier includes 2,000+ icons)
|
|
- **Emojis:** [unicode.org/emoji](https://unicode.org/emoji/charts/full-emoji-list.html)
|
|
|
|
## 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:
|
|
|
|
```json
|
|
"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!** 🚀
|
|
|