-5. generatePluginConfigForm() creates form from schema
-6. Current config values populated into form
-```
-
-### Form Generation Logic
-
-Based on JSON Schema `type`:
-
-- **boolean** → Toggle switch
-- **number/integer** → Number input with min/max
-- **string** → Text input with maxLength
-- **array** → Comma-separated text input
-- **enum** → Dropdown select
-
-### Save Process
-
-1. User submits form
-2. `savePluginConfiguration()` processes form data:
- - Converts types per schema (parseInt, parseFloat, split for arrays)
- - Handles boolean checkbox state
-3. Each field sent to `/api/plugins/config` individually
-4. Backend updates `config.json`
-5. Success notification shown
-6. Plugins refreshed to update display
-
-## Benefits
-
-### For Users
-
-- **Organized UI**: Plugin management separate from configuration
-- **Better UX**: Each plugin has its own dedicated space
-- **Type Safety**: Inputs validated based on schema constraints
-- **Easy Reset**: One-click reset to defaults
-- **Clear Labels**: Schema descriptions shown as help text
-
-### For Developers
-
-- **Automatic**: No custom UI code needed
-- **Declarative**: Just define JSON Schema
-- **Flexible**: Supports all common data types
-- **Validated**: Schema constraints enforced automatically
-
-## Key Features
-
-1. **Dynamic Tab Creation**: Tabs appear/disappear as plugins are installed/uninstalled
-2. **JSON Schema Driven**: Forms generated from standard JSON Schema
-3. **Type Conversion**: Automatic conversion between HTML form strings and config types
-4. **Default Values**: Schema defaults used when config value missing
-5. **Backward Compatible**: Plugins without schemas still work normally
-
-## File Structure
-
-```
-LEDMatrix/
-├── web_interface_v2.py # Backend API changes
-├── templates/
-│ └── index_v2.html # Frontend tab generation
-└── docs/
- ├── PLUGIN_CONFIGURATION_TABS.md # Full documentation
- └── PLUGIN_CONFIG_TABS_SUMMARY.md # This file
-
-plugins/
-├── hello-world/
-│ ├── manifest.json # References config_schema.json
-│ └── config_schema.json # Defines configuration structure
-└── clock-simple/
- ├── manifest.json
- └── config_schema.json
-```
-
-## Usage Example
-
-### For Users
-
-1. Install a plugin via Plugin Store
-2. Navigate to Plugins tab
-3. Click "Configure" on plugin card
-4. Plugin's configuration tab opens automatically
-5. Modify settings and click "Save Configuration"
-6. Restart display to apply changes
-
-### For Plugin Developers
-
-Create `config_schema.json`:
-
-```json
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "properties": {
- "enabled": {
- "type": "boolean",
- "default": true
- },
- "message": {
- "type": "string",
- "default": "Hello!",
- "maxLength": 50
- }
- }
-}
-```
-
-Reference in `manifest.json`:
-
-```json
-{
- "id": "my-plugin",
- "name": "My Plugin",
- "icon": "fas fa-star", // Optional: custom icon
- "config_schema": "config_schema.json"
-}
-```
-
-That's it! The configuration tab will be automatically generated.
-
-**Tip:** Add an `icon` field to customize your plugin's tab icon. Supports Font Awesome icons, emoji, or custom images. See [PLUGIN_CUSTOM_ICONS.md](PLUGIN_CUSTOM_ICONS.md) for details.
-
-## Testing Checklist
-
-- [x] Backend loads config schemas
-- [x] Tabs generated for installed plugins
-- [x] Forms render all field types correctly
-- [x] Current values populated
-- [x] Save updates config.json
-- [x] Type conversion works (string → number, string → array)
-- [x] Reset to defaults works
-- [x] Configure button navigates to tab
-- [x] Tabs removed when plugin uninstalled
-- [x] Backward compatible with plugins without schemas
-
-## Known Limitations
-
-1. **Nested Objects**: Only supports flat property structures
-2. **Conditional Fields**: No support for JSON Schema conditionals
-3. **Custom Validation**: Only basic schema validation supported
-4. **Array of Objects**: Arrays must be primitive types or simple lists
-
-## Future Improvements
-
-1. Support nested object properties
-2. Add visual validation feedback
-3. Color picker for RGB arrays
-4. File upload support for assets
-5. Configuration presets/templates
-6. Export/import configurations
-7. Plugin-specific custom renderers
-
-## Migration Notes
-
-- Existing plugins continue to work without changes
-- Plugins with `config_schema.json` automatically get tabs
-- No breaking changes to existing APIs
-- The Plugins tab still handles management operations
-- Raw JSON editor still available as fallback
-
-## Related Documentation
-
-- [PLUGIN_CONFIGURATION_TABS.md](PLUGIN_CONFIGURATION_TABS.md) - Full user and developer guide
-- [Plugin Store Documentation](plugin_docs/) - Plugin system overview
-- [JSON Schema Draft 07](https://json-schema.org/draft-07/schema) - Schema specification
-
diff --git a/docs/archive/PLUGIN_CUSTOM_ICONS_FEATURE.md b/docs/archive/PLUGIN_CUSTOM_ICONS_FEATURE.md
deleted file mode 100644
index fd7c2ddb..00000000
--- a/docs/archive/PLUGIN_CUSTOM_ICONS_FEATURE.md
+++ /dev/null
@@ -1,434 +0,0 @@
-# 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 `
`;
- }
-
- // 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 '
';
-}
-```
-
-**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!** 🚀
-
diff --git a/docs/archive/PLUGIN_DISPATCH_IMPLEMENTATION.md b/docs/archive/PLUGIN_DISPATCH_IMPLEMENTATION.md
deleted file mode 100644
index 89ccece8..00000000
--- a/docs/archive/PLUGIN_DISPATCH_IMPLEMENTATION.md
+++ /dev/null
@@ -1,144 +0,0 @@
-# Plugin-First Dispatch Implementation
-
-## Summary
-
-Successfully implemented a minimal, zero-risk plugin dispatch system that allows plugins to work seamlessly alongside legacy managers without refactoring existing code.
-
-## Changes Made
-
-### 1. Plugin Modes Dictionary (Lines 393, 422-425)
-Added `self.plugin_modes = {}` dictionary to track mode-to-plugin mappings:
-```python
-self.plugin_modes = {} # mode -> plugin_instance mapping for plugin-first dispatch
-```
-
-During plugin loading, each plugin's display modes are registered:
-```python
-for mode in display_modes:
- self.plugin_modes[mode] = plugin_instance
- logger.info(f"Registered plugin mode: {mode} -> {plugin_id}")
-```
-
-### 2. Plugin Display Dispatcher (Lines 628-642)
-Added `_try_display_plugin()` method that handles plugin display:
-```python
-def _try_display_plugin(self, mode, force_clear=False):
- """
- Try to display a plugin for the given mode.
- Returns True if plugin handled it, False if should fall through to legacy.
- """
- plugin = self.plugin_modes.get(mode)
- if not plugin:
- return False
-
- try:
- plugin.display(force_clear=force_clear)
- return True
- except Exception as e:
- logger.error(f"Error displaying plugin for mode {mode}: {e}", exc_info=True)
- return False
-```
-
-### 3. Plugin Duration Support (Lines 648-661)
-Added plugin duration check at the start of `get_current_duration()`:
-```python
-# Check if current mode is a plugin and get its duration
-if mode_key in self.plugin_modes:
- try:
- plugin = self.plugin_modes[mode_key]
- duration = plugin.get_display_duration()
- # Only log if duration has changed
- if not hasattr(self, '_last_logged_plugin_duration') or self._last_logged_plugin_duration != (mode_key, duration):
- logger.info(f"Using plugin duration for {mode_key}: {duration} seconds")
- self._last_logged_plugin_duration = (mode_key, duration)
- return duration
- except Exception as e:
- logger.error(f"Error getting plugin duration for {mode_key}: {e}")
- return self.display_durations.get(mode_key, 15)
-```
-
-### 4. Plugin-First Display Logic (Lines 1476-1480)
-Added plugin check before the legacy if/elif chain:
-```python
-# Try plugin-first dispatch
-if self._try_display_plugin(self.current_display_mode, force_clear=self.force_clear):
- # Plugin handled it, reset force_clear and continue
- if self.force_clear:
- self.force_clear = False
-elif self.current_display_mode == 'music' and self.music_manager:
- # Existing legacy code continues...
-```
-
-### 5. Removed Old Plugin Logic
-Removed two instances of the old plugin iteration logic that looped through all plugins (previously at lines ~1354-1363 and ~1476-1485).
-
-## Total Impact
-
-- **Lines Added**: ~36 lines of new code
-- **Lines Removed**: ~20 lines of old plugin iteration code
-- **Net Change**: +16 lines
-- **Files Modified**: 1 file (`src/display_controller.py`)
-- **Files Created**: 0
-- **Breaking Changes**: None
-
-## How It Works
-
-1. **Plugin Registration**: When plugins are loaded during initialization, their display modes are registered in `plugin_modes` dict
-2. **Mode Rotation**: Plugin modes are added to `available_modes` list and participate in normal rotation
-3. **Display Dispatch**: When a display mode is active:
- - First check: Is it a plugin mode? → Call `plugin.display()`
- - If not: Fall through to existing legacy if/elif chain
-4. **Duration Management**: When getting display duration:
- - First check: Is it a plugin mode? → Call `plugin.get_display_duration()`
- - If not: Use existing legacy duration logic
-
-## Benefits
-
-✅ **Zero Risk**: All legacy code paths remain intact and unchanged
-✅ **Minimal Code**: Only ~36 new lines added
-✅ **Works Immediately**: Plugins now work seamlessly with legacy managers
-✅ **No Refactoring**: No changes to working code
-✅ **Easy to Test**: Only need to test plugin dispatch, legacy is unchanged
-✅ **Gradual Migration**: Can migrate managers to plugins one-by-one
-✅ **Error Handling**: Plugin errors don't crash the system
-
-## Testing Checklist
-
-- [x] No linting errors
-- [ ] Test plugins display correctly in rotation
-- [ ] Test legacy managers still work correctly
-- [ ] Test mode switching between plugin and legacy
-- [ ] Test plugin duration handling
-- [ ] Test plugin error handling (plugin crashes don't affect system)
-- [ ] Test on actual Raspberry Pi hardware
-
-## Future Migration Path
-
-When migrating a legacy manager to a plugin:
-1. Create the plugin version in `plugins/`
-2. Enable the plugin in config
-3. Disable the legacy manager in config
-4. Test
-5. Eventually remove legacy manager initialization code
-
-**No changes to display loop needed!** The plugin-first dispatch automatically handles it.
-
-## Example: Current Behavior
-
-**With hello-world plugin enabled:**
-```
-[INFO] Registered plugin mode: hello-world -> hello-world
-[INFO] Added plugin mode to rotation: hello-world
-[INFO] Available display modes: ['clock', 'weather_current', ..., 'hello-world']
-[INFO] Showing hello-world
-[INFO] Using plugin duration for hello-world: 15 seconds
-```
-
-**Plugin displays, then rotates to next mode (e.g., clock):**
-```
-[INFO] Switching to clock from hello-world
-[INFO] Showing clock
-```
-
-**Everything works together seamlessly!**
-
diff --git a/docs/archive/PLUGIN_SCHEMA_AUDIT_SUMMARY.md b/docs/archive/PLUGIN_SCHEMA_AUDIT_SUMMARY.md
deleted file mode 100644
index 5424c300..00000000
--- a/docs/archive/PLUGIN_SCHEMA_AUDIT_SUMMARY.md
+++ /dev/null
@@ -1,157 +0,0 @@
-# Plugin Config Schema Audit and Standardization - Summary
-
-## Overview
-
-Completed comprehensive audit and standardization of all 12 plugin configuration schemas in the LEDMatrix project.
-
-## Results
-
-### Validation Status
-- ✅ **All 12 schemas pass JSON Schema Draft-07 validation**
-- ✅ **All schemas successfully load via SchemaManager**
-- ✅ **All schemas generate default configurations correctly**
-
-### Standardization Achievements
-
-1. **Common Fields Standardized**
- - ✅ All plugins now have `enabled` as the first property
- - ✅ All plugins have standardized `display_duration` field (where applicable)
- - ✅ Added `live_priority` to plugins that support live content
- - ✅ Added `high_performance_transitions` to all plugins
- - ✅ Added `transition` object to all plugins
- - ✅ Standardized `update_interval` naming (replaced `update_interval_seconds` where appropriate)
-
-2. **Metadata Improvements**
- - ✅ Added `title` field to all schemas (12/12)
- - ✅ Added `description` field to all schemas (12/12)
- - ✅ Improved descriptions to be clearer and more user-friendly
-
-3. **Property Ordering**
- - ✅ All schemas follow consistent ordering: common fields first, then plugin-specific
- - ✅ Order: `enabled` → `display_duration` → `live_priority` → `high_performance_transitions` → `update_interval` → `transition` → plugin-specific
-
-4. **Formatting**
- - ✅ Consistent 2-space indentation throughout
- - ✅ Consistent spacing and structure
- - ✅ All schemas use `additionalProperties: false` for strict validation
-
-## Plugins Updated
-
-1. **baseball-scoreboard** - Added common fields, standardized naming
-2. **clock-simple** - Added title, description, common fields, improved descriptions
-3. **football-scoreboard** - Reordered properties (enabled first), added common fields, standardized naming
-4. **hockey-scoreboard** - Added title, description, common fields, standardized naming
-5. **ledmatrix-flights** - Added common fields
-6. **ledmatrix-leaderboard** - Added common fields, moved update_interval to top level
-7. **ledmatrix-stocks** - Added common fields, fixed update_interval type
-8. **ledmatrix-weather** - Added missing `enabled` field, added title/description, reordered properties, added common fields
-9. **odds-ticker** - Added common fields
-10. **static-image** - Added title and description
-11. **text-display** - Added title, description, common fields, improved descriptions
-
-## Key Changes by Plugin
-
-### clock-simple
-- Added title and description
-- Added `live_priority`, `high_performance_transitions`, `transition`
-- Improved field descriptions
-- Reordered properties
-
-### text-display
-- Added title and description
-- Added `live_priority`, `high_performance_transitions`, `update_interval`, `transition`
-- Improved field descriptions
-- Reordered properties
-
-### ledmatrix-weather
-- **Critical fix**: Added missing `enabled` field (was completely missing)
-- Added title and description
-- Reordered properties (enabled first)
-- Added `live_priority`, `high_performance_transitions`, `transition`
-- Added `enabled` to required fields
-
-### football-scoreboard
-- Reordered properties (enabled first)
-- Renamed `update_interval_seconds` to `update_interval` at top level
-- Added `live_priority`, `high_performance_transitions`, `transition`
-- Added `enabled` to required fields
-- Improved title and description
-
-### hockey-scoreboard
-- Added title and description
-- Renamed top-level `update_interval_seconds` to `update_interval`
-- Added `live_priority`, `high_performance_transitions`, `transition`
-- Note: Nested league configs still use `update_interval_seconds` (intentional for clarity in nested contexts)
-
-### baseball-scoreboard
-- Renamed `update_interval_seconds` to `update_interval` at top level
-- Added `high_performance_transitions`, `transition`
-- Note: Nested league configs still use `update_interval_seconds` (intentional)
-
-### ledmatrix-leaderboard
-- Added `display_duration`, `live_priority`, `high_performance_transitions`, `update_interval`, `transition` at top level
-- Removed duplicate `update_interval` from `global` object (moved to top level)
-
-### ledmatrix-stocks
-- Changed `update_interval` type from `number` to `integer`
-- Added `live_priority`, `high_performance_transitions`, `transition`
-
-### odds-ticker
-- Added `live_priority`, `high_performance_transitions`, `transition`
-
-### ledmatrix-flights
-- Added `live_priority`, `high_performance_transitions`, `transition`
-
-### static-image
-- Added title and description
-
-## Notes on "Duplicates"
-
-The analysis script detected many "duplicate" fields, but these are **false positives**. The script flags nested objects with the same field names (e.g., `enabled` in multiple nested objects), which is **valid and expected** in JSON Schema. These are not actual duplicates - they're properly scoped within their respective object contexts.
-
-For example:
-- `enabled` at root level vs `enabled` in `nfl.enabled` - these are different properties in different contexts
-- `dynamic_duration` at root vs `nfl.dynamic_duration` - these are separate, valid nested configurations
-
-## Validation Alignment
-
-The `validate_config()` methods in plugin managers focus on business logic validation (e.g., timezone validation, enum checks), while the JSON Schema handles:
-- Type validation
-- Constraint validation (min/max, pattern matching)
-- Required field validation
-- Default value application
-
-This separation is correct and follows best practices.
-
-## Testing
-
-All schemas were verified to:
-1. ✅ Pass JSON Schema Draft-07 validation
-2. ✅ Load successfully via SchemaManager
-3. ✅ Generate default configurations correctly
-4. ✅ Have consistent formatting and structure
-
-## Next Steps (Optional)
-
-1. Consider updating plugin manager code that uses `update_interval_seconds` to use `update_interval` for consistency (if not in nested contexts)
-2. Review validate_config() methods to ensure they align with schema constraints (most already do)
-3. Consider adding more detailed enum descriptions where helpful
-
-## Files Modified
-
-- `plugins/baseball-scoreboard/config_schema.json`
-- `plugins/clock-simple/config_schema.json`
-- `plugins/football-scoreboard/config_schema.json`
-- `plugins/hockey-scoreboard/config_schema.json`
-- `plugins/ledmatrix-flights/config_schema.json`
-- `plugins/ledmatrix-leaderboard/config_schema.json`
-- `plugins/ledmatrix-stocks/config_schema.json`
-- `plugins/ledmatrix-weather/config_schema.json`
-- `plugins/odds-ticker/config_schema.json`
-- `plugins/static-image/config_schema.json`
-- `plugins/text-display/config_schema.json`
-
-## Analysis Script
-
-Created `scripts/analyze_plugin_schemas.py` for ongoing schema validation and analysis.
-
diff --git a/docs/archive/PLUGIN_STORE_QUICK_REFERENCE.md b/docs/archive/PLUGIN_STORE_QUICK_REFERENCE.md
deleted file mode 100644
index a48ecdbf..00000000
--- a/docs/archive/PLUGIN_STORE_QUICK_REFERENCE.md
+++ /dev/null
@@ -1,167 +0,0 @@
-# Plugin Store - Quick Reference Card
-
-## For Users
-
-### Install Plugin from Store
-```bash
-# Web UI: Plugin Store → Search → Click Install
-# API:
-curl -X POST http://pi:5050/api/plugins/install \
- -d '{"plugin_id": "clock-simple"}'
-```
-
-### Install Plugin from GitHub URL ⭐
-```bash
-# Web UI: Plugin Store → "Install from URL" → Paste URL
-# API:
-curl -X POST http://pi:5050/api/plugins/install-from-url \
- -d '{"repo_url": "https://github.com/user/ledmatrix-plugin"}'
-```
-
-### Search Plugins
-```bash
-# Web UI: Use search bar and filters
-# API:
-curl "http://pi:5050/api/plugins/store/search?q=hockey&category=sports"
-```
-
-### List Installed
-```bash
-curl "http://pi:5050/api/plugins/installed"
-```
-
-### Enable/Disable
-```bash
-curl -X POST http://pi:5050/api/plugins/toggle \
- -d '{"plugin_id": "clock-simple", "enabled": true}'
-```
-
-### Update Plugin
-```bash
-curl -X POST http://pi:5050/api/plugins/update \
- -d '{"plugin_id": "clock-simple"}'
-```
-
-### Uninstall
-```bash
-curl -X POST http://pi:5050/api/plugins/uninstall \
- -d '{"plugin_id": "clock-simple"}'
-```
-
-## For Developers
-
-### Share Your Plugin
-```markdown
-1. Create plugin following manifest structure
-2. Push to GitHub: https://github.com/you/ledmatrix-your-plugin
-3. Share URL with users:
- "Install my plugin from: https://github.com/you/ledmatrix-your-plugin"
-4. Users paste URL in "Install from URL" section
-```
-
-### Python Usage
-```python
-from src.plugin_system.store_manager import PluginStoreManager
-
-store = PluginStoreManager()
-
-# Install from URL
-result = store.install_from_url('https://github.com/user/plugin')
-if result['success']:
- print(f"Installed: {result['plugin_id']}")
-
-# Install from registry
-store.install_plugin('clock-simple')
-
-# Search
-results = store.search_plugins(query='hockey', category='sports')
-
-# List installed
-for plugin_id in store.list_installed_plugins():
- info = store.get_installed_plugin_info(plugin_id)
- print(f"{plugin_id}: {info['name']}")
-```
-
-## Required Plugin Structure
-
-```
-my-plugin/
-├── manifest.json # Required: Plugin metadata
-├── manager.py # Required: Plugin class
-├── requirements.txt # Optional: Python dependencies
-├── config_schema.json # Optional: Config validation
-├── README.md # Recommended: Documentation
-└── assets/ # Optional: Logos, fonts, etc.
-```
-
-### Minimal manifest.json
-```json
-{
- "id": "my-plugin",
- "name": "My Plugin",
- "version": "1.0.0",
- "author": "Your Name",
- "description": "What it does",
- "entry_point": "manager.py",
- "class_name": "MyPlugin",
- "category": "custom"
-}
-```
-
-## Key Features
-
-✅ **Install from Official Registry** - Curated, verified plugins
-✅ **Install from GitHub URL** - Any repo, instant install
-✅ **Search & Filter** - Find plugins by category, tags, query
-✅ **Auto Dependencies** - requirements.txt installed automatically
-✅ **Git or ZIP** - Git clone preferred, ZIP fallback
-✅ **Update System** - Keep plugins current
-✅ **Safe Uninstall** - Clean removal
-
-## Safety Notes
-
-⚠️ **Verified** (✓) = Reviewed by maintainers, safe
-⚠️ **Unverified** = From custom URL, review before installing
-⚠️ **Always** review plugin code before installing from URL
-⚠️ **Only** install from sources you trust
-
-## Common Issues
-
-**"Failed to clone"**
-→ Check git is installed: `which git`
-→ Verify GitHub URL is correct
-→ System will try ZIP download as fallback
-
-**"No manifest.json"**
-→ Plugin repo must have manifest.json in root
-→ Check repo structure
-
-**"Dependencies failed"**
-→ Manually install: `pip3 install -r plugins/plugin-id/requirements.txt`
-
-**Plugin won't load**
-→ Check enabled in config: `"enabled": true`
-→ Restart display: `sudo systemctl restart ledmatrix`
-→ Check logs: `sudo journalctl -u ledmatrix -f`
-
-## Documentation
-
-- Full Guide: `PLUGIN_STORE_USER_GUIDE.md`
-- Implementation: `PLUGIN_STORE_IMPLEMENTATION_SUMMARY.md`
-- Architecture: `PLUGIN_ARCHITECTURE_SPEC.md`
-- Developer Guide: `PLUGIN_DEVELOPER_GUIDE.md` (coming soon)
-
-## Support
-
-- Report issues on GitHub
-- Check wiki for troubleshooting
-- Join community discussions
-
----
-
-**Quick Tip**: To install your own plugin for testing:
-1. Push to GitHub
-2. Paste URL in web interface
-3. Click install
-4. Done!
-
diff --git a/docs/archive/PLUGIN_STORE_USER_GUIDE.md b/docs/archive/PLUGIN_STORE_USER_GUIDE.md
deleted file mode 100644
index dac73b6c..00000000
--- a/docs/archive/PLUGIN_STORE_USER_GUIDE.md
+++ /dev/null
@@ -1,450 +0,0 @@
-# LEDMatrix Plugin Store - User Guide
-
-## Overview
-
-The LEDMatrix Plugin Store allows you to easily discover, install, and manage display plugins for your LED matrix. You can install curated plugins from the official registry or add custom plugins directly from any GitHub repository.
-
-## Two Ways to Install Plugins
-
-### Method 1: From Official Plugin Store (Recommended)
-
-The official plugin store contains curated, verified plugins that have been reviewed by maintainers.
-
-**Via Web UI:**
-1. Open the web interface (http://your-pi-ip:5050)
-2. Navigate to "Plugin Store" tab
-3. Browse or search for plugins
-4. Click "Install" on the plugin you want
-5. Wait for installation to complete
-6. Restart the display to activate the plugin
-
-**Via API:**
-```bash
-curl -X POST http://your-pi-ip:5050/api/plugins/install \
- -H "Content-Type: application/json" \
- -d '{"plugin_id": "clock-simple"}'
-```
-
-**Via Python:**
-```python
-from src.plugin_system.store_manager import PluginStoreManager
-
-store = PluginStoreManager()
-success = store.install_plugin('clock-simple')
-if success:
- print("Plugin installed!")
-```
-
-### Method 2: From Custom GitHub URL
-
-Install any plugin directly from a GitHub repository, even if it's not in the official store. This is perfect for:
-- Testing your own plugins during development
-- Installing community plugins before they're in the official store
-- Using private plugins
-- Sharing plugins with specific users
-
-**Via Web UI:**
-1. Open the web interface
-2. Navigate to "Plugin Store" tab
-3. Find the "Install from URL" section at the bottom
-4. Paste the GitHub repository URL (e.g., `https://github.com/user/ledmatrix-my-plugin`)
-5. Click "Install from URL"
-6. Review the warning about unverified plugins
-7. Confirm installation
-8. Wait for installation to complete
-9. Restart the display
-
-**Via API:**
-```bash
-curl -X POST http://your-pi-ip:5050/api/plugins/install-from-url \
- -H "Content-Type: application/json" \
- -d '{"repo_url": "https://github.com/user/ledmatrix-my-plugin"}'
-```
-
-**Via Python:**
-```python
-from src.plugin_system.store_manager import PluginStoreManager
-
-store = PluginStoreManager()
-result = store.install_from_url('https://github.com/user/ledmatrix-my-plugin')
-
-if result['success']:
- print(f"Installed: {result['plugin_id']}")
-else:
- print(f"Error: {result['error']}")
-```
-
-## Searching for Plugins
-
-**Via Web UI:**
-- Use the search bar to search by name, description, or author
-- Filter by category (sports, weather, time, finance, etc.)
-- Click on tags to filter by specific tags
-
-**Via API:**
-```bash
-# Search by query
-curl "http://your-pi-ip:5050/api/plugins/store/search?q=hockey"
-
-# Filter by category
-curl "http://your-pi-ip:5050/api/plugins/store/search?category=sports"
-
-# Filter by tags
-curl "http://your-pi-ip:5050/api/plugins/store/search?tags=nhl&tags=hockey"
-```
-
-**Via Python:**
-```python
-from src.plugin_system.store_manager import PluginStoreManager
-
-store = PluginStoreManager()
-
-# Search by query
-results = store.search_plugins(query="hockey")
-
-# Filter by category
-results = store.search_plugins(category="sports")
-
-# Filter by tags
-results = store.search_plugins(tags=["nhl", "hockey"])
-```
-
-## Managing Installed Plugins
-
-### List Installed Plugins
-
-**Via Web UI:**
-- Navigate to "Plugin Manager" tab
-- See all installed plugins with their status
-
-**Via API:**
-```bash
-curl "http://your-pi-ip:5050/api/plugins/installed"
-```
-
-**Via Python:**
-```python
-from src.plugin_system.store_manager import PluginStoreManager
-
-store = PluginStoreManager()
-installed = store.list_installed_plugins()
-
-for plugin_id in installed:
- info = store.get_installed_plugin_info(plugin_id)
- print(f"{info['name']} (Last updated: {info.get('last_updated', 'unknown')})")
-```
-
-### Enable/Disable Plugins
-
-**Via Web UI:**
-1. Go to "Plugin Manager" tab
-2. Use the toggle switch next to each plugin
-3. Restart display to apply changes
-
-**Via API:**
-```bash
-curl -X POST http://your-pi-ip:5050/api/plugins/toggle \
- -H "Content-Type: application/json" \
- -d '{"plugin_id": "clock-simple", "enabled": true}'
-```
-
-### Update Plugins
-
-**Via Web UI:**
-1. Go to "Plugin Manager" tab
-2. Click "Update" button next to the plugin
-3. Wait for update to complete
-4. Restart display
-
-**Via API:**
-```bash
-curl -X POST http://your-pi-ip:5050/api/plugins/update \
- -H "Content-Type: application/json" \
- -d '{"plugin_id": "clock-simple"}'
-```
-
-**Via Python:**
-```python
-from src.plugin_system.store_manager import PluginStoreManager
-
-store = PluginStoreManager()
-success = store.update_plugin('clock-simple')
-```
-
-### Uninstall Plugins
-
-**Via Web UI:**
-1. Go to "Plugin Manager" tab
-2. Click "Uninstall" button next to the plugin
-3. Confirm removal
-4. Restart display
-
-**Via API:**
-```bash
-curl -X POST http://your-pi-ip:5050/api/plugins/uninstall \
- -H "Content-Type: application/json" \
- -d '{"plugin_id": "clock-simple"}'
-```
-
-**Via Python:**
-```python
-from src.plugin_system.store_manager import PluginStoreManager
-
-store = PluginStoreManager()
-success = store.uninstall_plugin('clock-simple')
-```
-
-## Configuring Plugins
-
-Each plugin can have its own configuration in `config/config.json`:
-
-```json
-{
- "clock-simple": {
- "enabled": true,
- "display_duration": 15,
- "color": [255, 255, 255],
- "time_format": "12h"
- },
- "nhl-scores": {
- "enabled": true,
- "favorite_teams": ["TBL", "FLA"],
- "show_favorite_teams_only": true
- }
-}
-```
-
-**Via Web UI:**
-1. Go to "Plugin Manager" tab
-2. Click the ⚙️ Configure button next to the plugin
-3. Edit configuration in the form
-4. Save changes
-5. Restart display to apply
-
-## Safety and Security
-
-### Verified vs Unverified Plugins
-
-- **✓ Verified Plugins**: Reviewed by maintainers, follow best practices, no known security issues
-- **⚠ Unverified Plugins**: User-contributed, not reviewed, install at your own risk
-
-When installing from a custom GitHub URL, you'll see a warning:
-
-```
-⚠️ WARNING: Installing Unverified Plugin
-
-You are about to install a plugin from a custom GitHub URL that has not been
-verified by the LEDMatrix maintainers. Only install plugins from sources you trust.
-
-Plugin will have access to:
-- Your display manager
-- Your cache manager
-- Configuration files
-- Network access (if plugin makes API calls)
-
-Repo: https://github.com/unknown-user/plugin-name
-```
-
-### Best Practices
-
-1. **Only install plugins from trusted sources**
-2. **Review plugin code before installing** (click "View on GitHub")
-3. **Check plugin ratings and reviews** (when available)
-4. **Keep plugins updated** for security patches
-5. **Report suspicious plugins** to maintainers
-
-## Troubleshooting
-
-### Plugin Won't Install
-
-**Problem:** Installation fails with "Failed to clone or download repository"
-
-**Solutions:**
-- Check that git is installed: `which git`
-- Verify the GitHub URL is correct
-- Check your internet connection
-- Try installing via download if git fails
-
-### Plugin Won't Load
-
-**Problem:** Plugin installed but doesn't appear in rotation
-
-**Solutions:**
-1. Check that plugin is enabled in config: `"enabled": true`
-2. Verify manifest.json exists and is valid
-3. Check logs for errors: `sudo journalctl -u ledmatrix -f`
-4. Restart the display service: `sudo systemctl restart ledmatrix`
-
-### Dependencies Failed
-
-**Problem:** "Error installing dependencies" message
-
-**Solutions:**
-- Check that pip3 is installed
-- Manually install: `pip3 install --break-system-packages -r plugins/plugin-id/requirements.txt`
-- Check for conflicting package versions
-
-### Plugin Shows Errors
-
-**Problem:** Plugin loads but shows error message on display
-
-**Solutions:**
-1. Check plugin configuration is correct
-2. Verify API keys are set (if plugin needs them)
-3. Check plugin logs: `sudo journalctl -u ledmatrix -f | grep plugin-id`
-4. Report issue to plugin developer on GitHub
-
-## Command-Line Usage
-
-For advanced users, you can manage plugins via command line:
-
-```bash
-# Install from registry
-python3 -c "
-from src.plugin_system.store_manager import PluginStoreManager
-store = PluginStoreManager()
-store.install_plugin('clock-simple')
-"
-
-# Install from URL
-python3 -c "
-from src.plugin_system.store_manager import PluginStoreManager
-store = PluginStoreManager()
-result = store.install_from_url('https://github.com/user/plugin')
-print(result)
-"
-
-# List installed
-python3 -c "
-from src.plugin_system.store_manager import PluginStoreManager
-store = PluginStoreManager()
-for plugin_id in store.list_installed_plugins():
- info = store.get_installed_plugin_info(plugin_id)
- print(f"{plugin_id}: {info['name']} (Last updated: {info.get('last_updated', 'unknown')})")
-"
-
-# Uninstall
-python3 -c "
-from src.plugin_system.store_manager import PluginStoreManager
-store = PluginStoreManager()
-store.uninstall_plugin('clock-simple')
-"
-```
-
-## API Reference
-
-All API endpoints return JSON with this structure:
-
-```json
-{
- "status": "success" | "error",
- "message": "Human-readable message",
- "data": { ... } // Varies by endpoint
-}
-```
-
-### Endpoints
-
-| Method | Endpoint | Description |
-|--------|----------|-------------|
-| GET | `/api/plugins/store/list` | List all plugins in store |
-| GET | `/api/plugins/store/search` | Search for plugins |
-| GET | `/api/plugins/installed` | List installed plugins |
-| POST | `/api/plugins/install` | Install from registry |
-| POST | `/api/plugins/install-from-url` | Install from GitHub URL |
-| POST | `/api/plugins/uninstall` | Uninstall plugin |
-| POST | `/api/plugins/update` | Update plugin |
-| POST | `/api/plugins/toggle` | Enable/disable plugin |
-| POST | `/api/plugins/config` | Update plugin config |
-
-## Examples
-
-### Example 1: Install Clock Plugin
-
-```bash
-# Install
-curl -X POST http://192.168.1.100:5050/api/plugins/install \
- -H "Content-Type: application/json" \
- -d '{"plugin_id": "clock-simple"}'
-
-# Configure
-cat >> config/config.json << EOF
-{
- "clock-simple": {
- "enabled": true,
- "display_duration": 20,
- "time_format": "24h"
- }
-}
-EOF
-
-# Restart display
-sudo systemctl restart ledmatrix
-```
-
-### Example 2: Install Custom Plugin from GitHub
-
-```bash
-# Install your own plugin during development
-curl -X POST http://192.168.1.100:5050/api/plugins/install-from-url \
- -H "Content-Type: application/json" \
- -d '{"repo_url": "https://github.com/myusername/ledmatrix-my-custom-plugin"}'
-
-# Enable it
-curl -X POST http://192.168.1.100:5050/api/plugins/toggle \
- -H "Content-Type: application/json" \
- -d '{"plugin_id": "my-custom-plugin", "enabled": true}'
-
-# Restart
-sudo systemctl restart ledmatrix
-```
-
-### Example 3: Share Plugin with Others
-
-As a plugin developer, you can share your plugin with others even before it's in the official store:
-
-```markdown
-# Share this URL with users:
-https://github.com/yourusername/ledmatrix-awesome-plugin
-
-# Users install with:
-1. Go to LEDMatrix web interface
-2. Click "Plugin Store" tab
-3. Scroll to "Install from URL"
-4. Paste: https://github.com/yourusername/ledmatrix-awesome-plugin
-5. Click "Install from URL"
-```
-
-## FAQ
-
-**Q: Do I need to restart the display after installing a plugin?**
-A: Yes, plugins are loaded when the display controller starts.
-
-**Q: Can I install plugins while the display is running?**
-A: Yes, you can install anytime, but you must restart to load them.
-
-**Q: What happens if I install a plugin with the same ID as an existing one?**
-A: The existing copy will be replaced with the latest code from the repository.
-
-**Q: Can I install multiple versions of the same plugin?**
-A: No, each plugin ID maps to a single checkout of the repository's default branch.
-
-**Q: How do I update all plugins at once?**
-A: Currently, you need to update each plugin individually. Bulk update is planned for a future release.
-
-**Q: Can plugins access my API keys from config_secrets.json?**
-A: Yes, if a plugin needs API keys, it can access them like core managers do.
-
-**Q: How much disk space do plugins use?**
-A: Most plugins are small (1-5MB). Check individual plugin documentation.
-
-**Q: Can I create my own plugin?**
-A: Yes! See PLUGIN_DEVELOPER_GUIDE.md for instructions.
-
-## Support
-
-- **Documentation**: See PLUGIN_ARCHITECTURE_SPEC.md
-- **Issues**: Report bugs on GitHub
-- **Community**: Join discussions in Issues
-- **Developer Guide**: See PLUGIN_DEVELOPER_GUIDE.md for creating plugins
-
diff --git a/docs/archive/RECONNECT_AFTER_CAPTIVE_PORTAL_TESTING.md b/docs/archive/RECONNECT_AFTER_CAPTIVE_PORTAL_TESTING.md
deleted file mode 100644
index b575f8f6..00000000
--- a/docs/archive/RECONNECT_AFTER_CAPTIVE_PORTAL_TESTING.md
+++ /dev/null
@@ -1,361 +0,0 @@
-# Reconnecting to Internet After Captive Portal Testing
-
-If captive portal testing fails or you need to reconnect to your normal network, here are several methods to get back online.
-
-## Quick Reference
-
-**Before testing:** Always run `sudo ./scripts/verify_wifi_before_testing.sh` first!
-
-**If stuck:** Run `sudo ./scripts/emergency_reconnect.sh` for automated recovery.
-
-## Quick Recovery Methods
-
-### Method 1: Via Web Interface (If Accessible)
-
-If you can still access the web interface at `http://192.168.4.1:5000`:
-
-1. **Navigate to WiFi tab**
-2. **Click "Scan"** to find available networks
-3. **Select your network** from the dropdown
-4. **Enter your WiFi password**
-5. **Click "Connect"**
-6. **Wait for connection** - AP mode should automatically disable
-
-### Method 2: Via SSH (If You Have Direct Access)
-
-If you have SSH access to the Pi (via Ethernet, direct connection, or still connected to AP):
-
-```bash
-# Connect via SSH
-ssh user@192.168.4.1 # If connected to AP
-# OR
-ssh user@
# If on same network
-
-# Disable AP mode first
-sudo systemctl stop hostapd
-sudo systemctl stop dnsmasq
-
-# Connect to WiFi using nmcli
-sudo nmcli device wifi connect "YourNetworkName" password "YourPassword"
-
-# Or if you have a saved connection
-sudo nmcli connection up "YourNetworkName"
-```
-
-### Method 3: Via API Endpoints (If Web Interface Works)
-
-If the web interface is accessible but you can't use the UI:
-
-```bash
-# Connect to WiFi via API
-curl -X POST http://192.168.4.1:5000/api/v3/wifi/connect \
- -H "Content-Type: application/json" \
- -d '{"ssid": "YourNetworkName", "password": "YourPassword"}'
-
-# Disable AP mode
-curl -X POST http://192.168.4.1:5000/api/v3/wifi/ap/disable
-```
-
-### Method 4: Direct Command Line (Physical Access)
-
-If you have physical access to the Pi or a keyboard/monitor:
-
-```bash
-# Disable AP mode services
-sudo systemctl stop hostapd
-sudo systemctl stop dnsmasq
-
-# Check available networks
-nmcli device wifi list
-
-# Connect to your network
-sudo nmcli device wifi connect "YourNetworkName" password "YourPassword"
-
-# Verify connection
-nmcli device status
-ip addr show wlan0
-```
-
-### Method 5: Using Saved Network Configuration
-
-If you've previously connected to a network, it may be saved:
-
-```bash
-# List saved connections
-nmcli connection show
-
-# Activate a saved connection
-sudo nmcli connection up "YourSavedConnectionName"
-
-# Or by UUID
-sudo nmcli connection up
-```
-
-## Step-by-Step Recovery Procedure
-
-### Scenario 1: Still Connected to AP Network
-
-If you're still connected to "LEDMatrix-Setup":
-
-1. **Access web interface:**
- ```
- http://192.168.4.1:5000
- ```
-
-2. **Go to WiFi tab**
-
-3. **Connect to your network** using the interface
-
-4. **Wait for connection** - you'll be disconnected from AP
-
-5. **Reconnect to your new network** and access Pi at its new IP
-
-### Scenario 2: Can't Access Web Interface
-
-If web interface is not accessible:
-
-1. **SSH into Pi** (if possible):
- ```bash
- ssh user@192.168.4.1 # Via AP
- # OR via Ethernet if connected
- ```
-
-2. **Disable AP mode:**
- ```bash
- sudo systemctl stop hostapd dnsmasq
- ```
-
-3. **Connect to WiFi:**
- ```bash
- sudo nmcli device wifi connect "YourNetwork" password "YourPassword"
- ```
-
-4. **Verify connection:**
- ```bash
- nmcli device status
- ping -c 3 8.8.8.8 # Test internet connectivity
- ```
-
-### Scenario 3: No Network Access at All
-
-If you have no network access (AP not working, no Ethernet):
-
-1. **Physical access required:**
- - Connect keyboard and monitor to Pi
- - Or use serial console if available
-
-2. **Disable AP services:**
- ```bash
- sudo systemctl stop hostapd
- sudo systemctl stop dnsmasq
- sudo systemctl disable hostapd # Prevent auto-start
- sudo systemctl disable dnsmasq
- ```
-
-3. **Connect to WiFi manually:**
- ```bash
- sudo nmcli device wifi list
- sudo nmcli device wifi connect "YourNetwork" password "YourPassword"
- ```
-
-4. **Restart network services if needed:**
- ```bash
- sudo systemctl restart NetworkManager
- ```
-
-## Emergency Recovery Script
-
-Create this script for quick recovery:
-
-```bash
-#!/bin/bash
-# emergency_reconnect.sh - Emergency WiFi reconnection script
-
-echo "Emergency WiFi Reconnection"
-echo "=========================="
-
-# Stop AP mode
-echo "Stopping AP mode..."
-sudo systemctl stop hostapd 2>/dev/null
-sudo systemctl stop dnsmasq 2>/dev/null
-
-# List available networks
-echo ""
-echo "Available networks:"
-nmcli device wifi list
-
-# Prompt for network
-echo ""
-read -p "Enter network SSID: " SSID
-read -sp "Enter password: " PASSWORD
-echo ""
-
-# Connect
-echo "Connecting to $SSID..."
-sudo nmcli device wifi connect "$SSID" password "$PASSWORD"
-
-# Wait a moment
-sleep 3
-
-# Check status
-if nmcli device status | grep -q "connected"; then
- echo "✓ Connected successfully!"
- IP=$(ip addr show wlan0 | grep "inet " | awk '{print $2}' | cut -d/ -f1)
- echo "IP Address: $IP"
-else
- echo "✗ Connection failed. Check credentials and try again."
-fi
-```
-
-Save as `scripts/emergency_reconnect.sh` and make executable:
-```bash
-chmod +x scripts/emergency_reconnect.sh
-sudo ./scripts/emergency_reconnect.sh
-```
-
-## Preventing Issues
-
-### Before Testing
-
-1. **Save your current network connection:**
- ```bash
- # Your network should already be saved if you've connected before
- nmcli connection show
- ```
-
-2. **Note your Pi's IP address** on your normal network:
- ```bash
- hostname -I
- ```
-
-3. **Ensure you have alternative access:**
- - Ethernet cable (if available)
- - SSH access via another method
- - Physical access to Pi
-
-### During Testing
-
-1. **Keep a terminal/SSH session open** to the Pi
-2. **Test from a secondary device** (not your main computer)
-3. **Have the recovery commands ready**
-
-### After Testing
-
-1. **Verify internet connectivity:**
- ```bash
- ping -c 3 8.8.8.8
- curl -I https://www.google.com
- ```
-
-2. **Check Pi's new IP address:**
- ```bash
- hostname -I
- ip addr show wlan0
- ```
-
-3. **Update your SSH/config** if IP changed
-
-## Troubleshooting Reconnection
-
-### Issue: Can't Connect to Saved Network
-
-**Solution:**
-```bash
-# Remove old connection and reconnect
-nmcli connection delete "NetworkName"
-sudo nmcli device wifi connect "NetworkName" password "Password"
-```
-
-### Issue: AP Mode Won't Disable
-
-**Solution:**
-```bash
-# Force stop services
-sudo systemctl stop hostapd dnsmasq
-sudo systemctl disable hostapd dnsmasq
-
-# Kill processes if needed
-sudo pkill hostapd
-sudo pkill dnsmasq
-
-# Restart NetworkManager
-sudo systemctl restart NetworkManager
-```
-
-### Issue: WiFi Interface Stuck
-
-**Solution:**
-```bash
-# Reset WiFi interface
-sudo nmcli radio wifi off
-sleep 2
-sudo nmcli radio wifi on
-sleep 3
-
-# Try connecting again
-sudo nmcli device wifi connect "NetworkName" password "Password"
-```
-
-### Issue: No Networks Found
-
-**Solution:**
-```bash
-# Check WiFi is enabled
-nmcli radio wifi
-
-# Enable if off
-sudo nmcli radio wifi on
-
-# Check interface status
-ip link show wlan0
-
-# Restart NetworkManager
-sudo systemctl restart NetworkManager
-```
-
-## Quick Reference Commands
-
-```bash
-# Disable AP mode
-sudo systemctl stop hostapd dnsmasq
-
-# List WiFi networks
-nmcli device wifi list
-
-# Connect to network
-sudo nmcli device wifi connect "SSID" password "Password"
-
-# Check connection status
-nmcli device status
-
-# Get IP address
-hostname -I
-ip addr show wlan0
-
-# Test internet
-ping -c 3 8.8.8.8
-
-# Restart network services
-sudo systemctl restart NetworkManager
-```
-
-## Best Practices
-
-1. **Always test from a secondary device** - Keep your main computer on your normal network
-2. **Have Ethernet backup** - If available, keep Ethernet connected as fallback
-3. **Save network credentials** - Ensure your network is saved before testing
-4. **Document your Pi's IP** - Note the IP on your normal network before testing
-5. **Keep SSH session open** - Maintain an active SSH connection during testing
-6. **Test during safe times** - Don't test when you need immediate internet access
-
-## Recovery Checklist
-
-- [ ] Stop AP mode services (hostapd, dnsmasq)
-- [ ] Verify WiFi interface is available
-- [ ] Scan for available networks
-- [ ] Connect to your network
-- [ ] Verify connection status
-- [ ] Test internet connectivity
-- [ ] Note new IP address
-- [ ] Update any configurations that reference old IP
-
diff --git a/docs/archive/STARTUP_OPTIMIZATION_SUMMARY.md b/docs/archive/STARTUP_OPTIMIZATION_SUMMARY.md
deleted file mode 100644
index 375974a7..00000000
--- a/docs/archive/STARTUP_OPTIMIZATION_SUMMARY.md
+++ /dev/null
@@ -1,299 +0,0 @@
-# LED Matrix Startup Optimization Summary
-
-## Overview
-This document summarizes the startup performance optimizations implemented to reduce the LED matrix display startup time from **102 seconds to under 10 seconds** (90%+ improvement).
-
-## Implemented Optimizations
-
-### Phase 1: High-Impact Changes (90+ seconds savings)
-
-#### 1. Smart Dependency Checking with Marker Files ✅
-**Impact: ~90 seconds savings**
-
-**Problem**: Running `pip install -r requirements.txt` for every plugin on every startup, even when dependencies were already installed.
-
-**Solution**:
-- Added marker file system at `/var/cache/ledmatrix/plugin__deps_installed`
-- Tracks which plugins have had dependencies installed
-- Only installs dependencies on first load or when marker is missing
-- Marker created with timestamp after successful installation
-- Marker removed when plugin is uninstalled
-
-**Files Modified**:
-- `src/plugin_system/plugin_manager.py`:
- - Added `_get_dependency_marker_path()`
- - Added `_check_dependencies_installed()`
- - Added `_mark_dependencies_installed()`
- - Added `_remove_dependency_marker()`
- - Modified `load_plugin()` to check marker before installing
- - Modified `unload_plugin()` to remove marker
-
-**Utility Script**: `scripts/clear_dependency_markers.sh` - Clears all markers to force fresh check
-
-#### 2. Removed Cache Clear at Startup ✅
-**Impact: ~5-30 seconds savings**
-
-**Problem**: Clearing entire cache on startup forced fresh API calls for all plugins, defeating the purpose of caching.
-
-**Solution**:
-- Removed `cache_manager.clear_cache()` call from startup
-- Removed 5-second sleep waiting for data
-- Trust cache TTL mechanisms for staleness
-- Let plugins use cached data immediately at startup
-- Background updates will refresh naturally
-
-**Files Modified**:
-- `src/display_controller.py` (lines 447-452):
- - Removed cache clear and sleep
- - Added comment explaining fast startup approach
-
-### Phase 2: Quick Wins (8-10 seconds savings)
-
-#### 3. Enhanced Startup Progress Logging ✅
-**Impact: Visibility improvement (no performance change)**
-
-**Features**:
-- Shows plugin count and progress (1/9, 2/9, etc.)
-- Displays individual plugin load times
-- Shows cumulative progress percentage
-- Reports elapsed time
-- Uses ✓ and ✗ symbols for success/failure
-
-**Files Modified**:
-- `src/display_controller.py` (lines 109-192):
- - Added enabled plugin counting
- - Added per-plugin timing
- - Added progress percentage calculation
- - Enhanced logging with symbols
-
-#### 4. Lazy-Load Flight Tracker Aircraft Database ✅
-**Impact: ~8-10 seconds savings at startup**
-
-**Problem**: Loading 70MB aircraft database during plugin initialization, even if not immediately needed.
-
-**Solution**:
-- Defer database loading until first use
-- Added `_ensure_database_loaded()` method
-- Called automatically when database is first accessed
-- Tracks load state to avoid repeated attempts
-- Logs load time when it happens (during first display, not startup)
-
-**Files Modified**:
-- `plugins/ledmatrix-flights/manager.py`:
- - Modified `__init__()` to defer database loading
- - Added `_ensure_database_loaded()` method
- - Modified `_get_aircraft_info_from_database()` to lazy-load
-
-### Phase 3: Advanced Optimization (2-3 seconds savings)
-
-#### 5. Parallel Plugin Loading ✅
-**Impact: ~2-3 seconds savings**
-
-**Solution**:
-- Use `ThreadPoolExecutor` with 4 concurrent workers
-- Load plugins in parallel instead of serially
-- Process results as they complete
-- Thread-safe plugin registration
-
-**Files Modified**:
-- `src/display_controller.py` (lines 1-7, 109-192):
- - Added ThreadPoolExecutor import
- - Created `load_single_plugin()` helper function
- - Parallel execution with progress tracking
- - Error handling per plugin
-
-## Expected Performance Results
-
-### Baseline (Before Optimizations)
-- **Total startup time**: 102.27 seconds
-- Core initialization: 1.65 seconds (fast)
-- Plugin loading: 100.6 seconds (bottleneck)
- - Dependency checks: ~90 seconds
- - Flight tracker DB: ~8 seconds
- - Other init: ~2 seconds
-
-### After Phase 1
-- **Expected**: ~12 seconds (90% improvement)
-- Dependency checks: 0 seconds (after first run)
-- Cache clear removed: 5+ seconds saved
-- **Savings**: 90 seconds
-
-### After Phase 2
-- **Expected**: ~3-4 seconds (96% improvement)
-- Flight tracker DB lazy-loaded: 8-10 seconds saved
-- **Savings**: 98 seconds total
-
-### After Phase 3
-- **Expected**: ~2 seconds (98% improvement)
-- Parallel loading: 2-3 seconds saved
-- **Savings**: 100+ seconds total
-
-## Testing and Validation
-
-### On Development Machine
-```bash
-# Test with emulator
-./scripts/dev/run_emulator.sh
-
-# Check logs for timing information
-# Look for:
-# - "Loading X enabled plugin(s) in parallel"
-# - Individual plugin load times
-# - "Plugin system initialized in X.XXX seconds"
-# - "DisplayController initialization completed in X.XXX seconds"
-```
-
-### On Raspberry Pi
-
-```bash
-# Deploy changes
-cd /home/ledpi/LEDMatrix
-git pull origin plugins # or your branch
-
-# Restart service
-sudo systemctl restart ledmatrix
-
-# Check startup time
-journalctl -u ledmatrix -b | grep -E "(Starting DisplayController|DisplayController initialization completed|Plugin system initialized)"
-
-# Check for dependency installations (should only happen on first run)
-journalctl -u ledmatrix -b | grep "Installing dependencies"
-
-# Check marker files
-ls -la /var/cache/ledmatrix/plugin_*_deps_installed
-
-# Monitor live
-journalctl -u ledmatrix -f
-```
-
-### Benchmarking Commands
-
-```bash
-# Get startup time from latest boot
-journalctl -u ledmatrix -b | grep "DisplayController initialization completed"
-
-# Compare with previous boots
-journalctl -u ledmatrix --since "1 day ago" | grep "DisplayController initialization completed"
-
-# Check dependency marker status
-ls -lh /var/cache/ledmatrix/plugin_*_deps_installed
-```
-
-## Troubleshooting
-
-### Plugins Fail Due to Missing Dependencies
-
-**Symptoms**: Plugin fails to import with ModuleNotFoundError
-
-**Solution**:
-```bash
-# Clear markers to force fresh dependency install
-sudo /home/ledpi/LEDMatrix/scripts/clear_dependency_markers.sh
-
-# Restart service
-sudo systemctl restart ledmatrix
-```
-
-### Want to Force Dependency Reinstall for a Specific Plugin
-
-```bash
-# Remove marker for specific plugin
-sudo rm /var/cache/ledmatrix/plugin__deps_installed
-
-# Restart service
-sudo systemctl restart ledmatrix
-```
-
-### Revert to Old Behavior (No Optimizations)
-
-To temporarily disable optimizations for testing:
-
-1. **Re-enable dependency checks every time**:
- - Edit `src/plugin_system/plugin_manager.py`
- - Comment out the marker check in `load_plugin()`
-
-2. **Re-enable cache clear**:
- - Edit `src/display_controller.py`
- - Add back cache clear and sleep in `run()` method
-
-## Performance Metrics to Monitor
-
-### Startup Metrics
-- Total initialization time
-- Plugin loading time
-- Individual plugin load times
-- First display ready time
-
-### Runtime Metrics
-- Memory usage (should be similar)
-- CPU usage (should be similar)
-- Display performance (should be identical)
-- Plugin functionality (should be identical)
-
-### Regression Indicators
-- Plugins failing to load
-- Missing dependencies errors
-- Stale data at startup (acceptable - will refresh)
-- Crashes during parallel loading
-
-## Rollback Plan
-
-If issues are encountered:
-
-1. **Revert Git commits**:
- ```bash
- git revert
- sudo systemctl restart ledmatrix
- ```
-
-2. **Cherry-pick safe changes**:
- - Keep progress logging (safe)
- - Keep lazy-load flight tracker (safe)
- - Revert parallel loading if issues
- - Revert dependency markers if issues
-
-3. **Emergency rollback**:
- ```bash
- git checkout
- sudo systemctl restart ledmatrix
- ```
-
-## Success Criteria
-
-✅ Startup time reduced to under 10 seconds (from 102 seconds)
-✅ All plugins load successfully
-✅ All display modes function correctly
-✅ No regression in display quality or performance
-✅ Cached data used effectively at startup
-✅ Dependencies installed correctly on first run
-✅ Progress logging shows clear startup status
-
-## Files Modified Summary
-
-1. `src/plugin_system/plugin_manager.py` - Dependency marker system
-2. `src/display_controller.py` - Cache removal, progress logging, parallel loading
-3. `plugins/ledmatrix-flights/manager.py` - Lazy-load aircraft database
-4. `scripts/clear_dependency_markers.sh` - Utility script (new)
-
-## Maintenance Notes
-
-- **Dependency markers persist** across restarts - this is intentional
-- **Clear markers** when updating plugin dependencies
-- **Cache remains** across restarts - data refreshes via TTL
-- **Parallel loading** is safe due to plugin independence
-- **Progress logs** help diagnose slow plugins
-
-## Future Optimization Opportunities
-
-1. **Lazy-load other heavy resources** (e.g., stock logos, team logos)
-2. **Background plugin loading** - start display immediately, load remaining plugins in background
-3. **Plugin load prioritization** - load frequently-used plugins first
-4. **Cached manifest reading** - avoid re-parsing JSON on every startup
-5. **Optimized font loading** - lazy-load fonts per plugin
-
----
-
-**Implementation Date**: November 9, 2025
-**Version**: 1.0
-**Status**: ✅ Ready for Pi Deployment
-
diff --git a/docs/archive/STATIC_IMAGE_MULTI_UPLOAD_PLAN.md b/docs/archive/STATIC_IMAGE_MULTI_UPLOAD_PLAN.md
deleted file mode 100644
index 0a6b9fc7..00000000
--- a/docs/archive/STATIC_IMAGE_MULTI_UPLOAD_PLAN.md
+++ /dev/null
@@ -1,378 +0,0 @@
-# Static Image Plugin - Multi-Image Upload & Rotation Implementation Plan
-
-## Overview
-
-Enhance the static-image plugin to support:
-1. **Multiple image uploads** via web UI
-2. **Image rotation** (sequential, random, time-based, date-based)
-3. **Robust asset management** (storage, validation, cleanup)
-4. **Future-proof architecture** for advanced rotation logic
-
-## Architecture Design
-
-### 1. Configuration Schema Enhancement
-
-#### Current Schema
-```json
-{
- "image_path": "assets/static_images/default.png"
-}
-```
-
-#### Enhanced Schema (Backward Compatible)
-```json
-{
- "image_config": {
- "mode": "single" | "multiple",
- "rotation_mode": "sequential" | "random" | "time_based" | "date_based",
- "images": [
- {
- "id": "uuid-or-hash",
- "path": "assets/plugins/static-image/uploads/image_1234567890.png",
- "uploaded_at": "2025-01-15T10:30:00Z",
- "display_order": 0,
- "schedule": null // Future: {"start_time": "08:00", "end_time": "18:00", "days": [1,2,3,4,5]}
- }
- ]
- },
-
- // Legacy support - maps to single image mode
- "image_path": "assets/static_images/default.png",
-
- // Rotation settings
- "rotation_settings": {
- "sequential_loop": true,
- "random_seed": null, // null = use time, or fixed seed for reproducible rotation
- "time_intervals": {
- "enabled": false,
- "interval_seconds": 3600 // Change image every hour
- },
- "date_ranges": [] // Future: [{"start": "2025-12-01", "end": "2025-12-25", "image_id": "..."}]
- }
-}
-```
-
-### 2. Asset Storage Structure
-
-```text
-assets/
-├── plugins/
-│ └── static-image/
-│ └── uploads/
-│ ├── image_1705312200_abc123.png
-│ ├── image_1705312400_def456.jpg
-│ └── .metadata.json // Maps IDs to filenames
-```
-
-**Storage Strategy:**
-- Files stored in `assets/plugins/static-image/uploads/`
-- Filenames: `image_{timestamp}_{hash}.{ext}` (prevents collisions)
-- Metadata JSON tracks: ID → filename mapping, upload dates, file sizes
-- Cleanup: Remove files not referenced in config
-
-### 3. Backend API Endpoints
-
-#### POST `/api/v3/plugins/assets/upload`
-**Purpose:** Upload image files for a specific plugin
-
-**Request:**
-- `multipart/form-data`
-- `plugin_id`: string (required)
-- `files`: File[] (multiple files supported)
-- `rotation_mode`: string (optional, default: "sequential")
-
-**Response:**
-```json
-{
- "status": "success",
- "uploaded_files": [
- {
- "id": "uuid-here",
- "filename": "image_1705312200_abc123.png",
- "path": "assets/plugins/static-image/uploads/image_1705312200_abc123.png",
- "size": 45678,
- "uploaded_at": "2025-01-15T10:30:00Z"
- }
- ]
-}
-```
-
-**Validation:**
-- File type: PNG, JPG, JPEG, BMP, GIF
-- Max file size: 5MB per file
-- Max files per upload: 10
-- Total storage limit: 50MB per plugin
-
-#### DELETE `/api/v3/plugins/assets/delete`
-**Purpose:** Delete uploaded image
-
-**Request:**
-- `plugin_id`: string
-- `image_id`: string (from upload response)
-
-**Response:**
-```json
-{
- "status": "success",
- "deleted_file": "image_1705312200_abc123.png"
-}
-```
-
-#### GET `/api/v3/plugins/assets/list`
-**Purpose:** List all uploaded images for a plugin
-
-**Response:**
-```json
-{
- "status": "success",
- "images": [
- {
- "id": "uuid-here",
- "filename": "image_1705312200_abc123.png",
- "path": "assets/plugins/static-image/uploads/image_1705312200_abc123.png",
- "size": 45678,
- "uploaded_at": "2025-01-15T10:30:00Z"
- }
- ]
-}
-```
-
-### 4. Frontend Form Generator Enhancement
-
-#### Schema Format for File Upload
-```json
-{
- "type": "object",
- "properties": {
- "images": {
- "type": "array",
- "x-widget": "file-upload",
- "x-upload-config": {
- "endpoint": "/api/v3/plugins/assets/upload",
- "plugin_id_field": "plugin_id",
- "max_files": 10,
- "allowed_types": ["image/png", "image/jpeg", "image/bmp", "image/gif"],
- "max_size_mb": 5
- },
- "items": {
- "type": "object",
- "properties": {
- "id": {"type": "string"},
- "path": {"type": "string"},
- "uploaded_at": {"type": "string", "format": "date-time"}
- }
- },
- "description": "Upload images to display. Multiple images will rotate based on rotation mode."
- },
- "rotation_mode": {
- "type": "string",
- "enum": ["sequential", "random", "time_based", "date_based"],
- "default": "sequential",
- "description": "How to rotate through images"
- }
- }
-}
-```
-
-#### UI Components
-1. **File Upload Widget:**
- - Drag-and-drop zone
- - File list with thumbnails
- - Remove button per file
- - Upload progress indicator
- - Image preview before upload
-
-2. **Rotation Mode Selector:**
- - Dropdown with rotation options
- - Settings panel per mode:
- - Sequential: Loop option
- - Random: Seed option
- - Time-based: Interval input
- - Date-based: Calendar picker (future)
-
-### 5. Plugin Manager Updates
-
-#### Rotation Logic in `manager.py`
-
-```python
-class StaticImagePlugin(BasePlugin):
- def __init__(self, ...):
- # ... existing code ...
-
- # Enhanced image handling
- self.image_config = config.get('image_config', {})
- self.rotation_mode = self.image_config.get('rotation_mode', 'sequential')
- self.rotation_settings = config.get('rotation_settings', {})
- self.images_list = self.image_config.get('images', [])
- self.current_image_index = 0
- self.last_rotation_time = time.time()
-
- # Initialize rotation
- self._setup_rotation()
-
- def _setup_rotation(self):
- """Initialize rotation based on mode"""
- if self.rotation_mode == 'random':
- import random
- seed = self.rotation_settings.get('random_seed')
- if seed:
- random.seed(seed)
-
- if not self.images_list:
- # Fallback to legacy image_path
- if self.image_path:
- self.images_list = [{'path': self.image_path}]
-
- def _get_next_image(self) -> Optional[str]:
- """Get next image path based on rotation mode"""
- if not self.images_list:
- return None
-
- if self.rotation_mode == 'sequential':
- path = self.images_list[self.current_image_index]['path']
- self.current_image_index = (self.current_image_index + 1) % len(self.images_list)
- return path
-
- elif self.rotation_mode == 'random':
- import random
- return random.choice(self.images_list)['path']
-
- elif self.rotation_mode == 'time_based':
- interval = self.rotation_settings.get('time_intervals', {}).get('interval_seconds', 3600)
- now = time.time()
- if now - self.last_rotation_time >= interval:
- self.current_image_index = (self.current_image_index + 1) % len(self.images_list)
- self.last_rotation_time = now
- return self.images_list[self.current_image_index]['path']
-
- elif self.rotation_mode == 'date_based':
- # Future implementation
- return self._get_date_based_image()
-
- return self.images_list[0]['path']
-
- def display(self, force_clear: bool = False):
- """Display current image based on rotation"""
- image_path = self._get_next_image()
- if not image_path or not os.path.exists(image_path):
- self._display_error()
- return
-
- self.image_path = image_path # For compatibility
- self._load_image()
-
- # ... rest of display logic ...
-```
-
-### 6. Asset Management System
-
-#### File Operations
-- **Upload:** Save to `assets/plugins/{plugin_id}/uploads/`
-- **Validation:** Check file type, size, dimensions
-- **Metadata:** Track in `.metadata.json`
-- **Cleanup:** Remove orphaned files on config save
-- **Permissions:** Ensure writable by web service
-
-#### Security
-- Validate file extensions (whitelist)
-- Check file content (magic bytes, not just extension)
-- Limit file sizes
-- Sanitize filenames
-- Prevent path traversal
-
-### 7. Migration Strategy
-
-#### Backward Compatibility
-1. **Legacy Support:**
- - If `image_path` exists but no `image_config`, auto-convert
- - Create `image_config` with single image from `image_path`
-
-2. **Config Migration:**
-```python
-def _migrate_legacy_config(self, config):
- """Migrate legacy image_path to new image_config format"""
- if 'image_path' in config and 'image_config' not in config:
- config['image_config'] = {
- 'mode': 'single',
- 'rotation_mode': 'sequential',
- 'images': [{
- 'id': str(uuid.uuid4()),
- 'path': config['image_path'],
- 'uploaded_at': datetime.now().isoformat(),
- 'display_order': 0
- }]
- }
- return config
-```
-
-## Implementation Phases
-
-### Phase 1: Core Upload System
-1. ✅ Enhanced config schema
-2. ✅ Backend upload endpoint
-3. ✅ Asset storage structure
-4. ✅ File validation
-
-### Phase 2: Frontend Integration
-5. ✅ File upload widget in form generator
-6. ✅ Image preview/management UI
-7. ✅ Rotation mode selector
-
-### Phase 3: Plugin Rotation Logic
-8. ✅ Update plugin manager with rotation
-9. ✅ Sequential rotation
-10. ✅ Random rotation
-
-### Phase 4: Advanced Features
-11. ✅ Time-based rotation
-12. ✅ Date-based rotation (future)
-13. ✅ Cleanup/orphan removal
-
-## File Structure Changes
-
-```text
-plugins/static-image/
-├── manager.py # Enhanced with rotation logic
-├── config_schema.json # Updated with upload/rotation fields
-├── manifest.json # No changes
-└── README.md # Update documentation
-
-web_interface/
-├── blueprints/
-│ └── api_v3.py # Add upload/delete/list endpoints
-└── templates/v3/
- └── partials/
- └── plugins.html # File upload widget
-
-assets/
-└── plugins/
- └── static-image/
- └── uploads/ # NEW - user uploaded images
- └── .metadata.json
-```
-
-## Testing Checklist
-
-- [ ] Single image upload works
-- [ ] Multiple image upload works
-- [ ] File validation (type, size)
-- [ ] Sequential rotation cycles correctly
-- [ ] Random rotation works
-- [ ] Time-based rotation changes at intervals
-- [ ] Legacy config migration preserves existing images
-- [ ] Orphaned file cleanup on config save
-- [ ] Web UI displays upload widget correctly
-- [ ] Image preview shows before upload
-- [ ] Delete removes file and updates config
-- [ ] Error handling for missing/invalid files
-
-## Future Enhancements
-
-1. **Date-based rotation:** Display different images on specific dates
-2. **Time-of-day rotation:** Show images based on time ranges
-3. **Transition effects:** Fade between images
-4. **Image filters:** Apply effects (brightness, contrast)
-5. **Bulk operations:** Select multiple images for deletion
-6. **Image organization:** Folders/tags for images
-7. **Remote images:** Support URLs (with caching)
-
diff --git a/docs/archive/TROUBLESHOOTING_QUICK_START.md b/docs/archive/TROUBLESHOOTING_QUICK_START.md
deleted file mode 100644
index 0569b0c8..00000000
--- a/docs/archive/TROUBLESHOOTING_QUICK_START.md
+++ /dev/null
@@ -1,92 +0,0 @@
-# Web Interface Troubleshooting - Quick Start
-
-## The Problem
-After reorganizing the web interface, it doesn't seem to run and shows no logging.
-
-## Why You're Not Seeing Logs
-
-**The web service logs to syslog, NOT stdout!**
-
-The systemd service is configured with:
-```
-StandardOutput=syslog
-StandardError=syslog
-SyslogIdentifier=ledmatrix-web
-```
-
-## Immediate Actions (Run on Raspberry Pi)
-
-### 1. Run the Diagnostic Script
-```bash
-ssh ledpi@
-cd ~/LEDMatrix
-bash scripts/diagnose_web_interface.sh
-```
-
-This automated script will check everything and tell you what's wrong.
-
-### 2. View the Actual Logs
-```bash
-# View recent logs
-sudo journalctl -u ledmatrix-web -n 50 --no-pager
-
-# Follow logs in real-time
-sudo journalctl -u ledmatrix-web -f
-```
-
-### 3. Check Service Status
-```bash
-sudo systemctl status ledmatrix-web
-```
-
-### 4. Try Manual Start (Best for Debugging)
-```bash
-cd ~/LEDMatrix
-python3 web_interface/start.py
-```
-
-This will show errors directly in your terminal.
-
-## Most Likely Issues
-
-### Issue 1: web_display_autostart is False
-The web interface is designed NOT to start if this config is false.
-
-**Fix:**
-```bash
-nano ~/LEDMatrix/config/config.json
-# Change: "web_display_autostart": true
-sudo systemctl restart ledmatrix-web
-```
-
-### Issue 2: Service Not Started
-**Fix:**
-```bash
-sudo systemctl start ledmatrix-web
-sudo systemctl enable ledmatrix-web
-```
-
-### Issue 3: Import Errors
-**Fix:**
-```bash
-cd ~/LEDMatrix
-pip3 install --break-system-packages -r web_interface/requirements.txt
-sudo systemctl restart ledmatrix-web
-```
-
-## Full Documentation
-
-- **Comprehensive Guide:** `docs/WEB_INTERFACE_TROUBLESHOOTING.md`
-- **Reorganization Info:** `WEB_INTERFACE_REORGANIZATION.md`
-
-## After Fixing
-
-Once it's working, you should see:
-- Service status: "active (running)" in green
-- Accessible at: `http://:5000`
-- Logs showing: "Starting LED Matrix Web Interface V3..."
-
-## Need Help?
-
-Run the diagnostic script and share its output - it will show exactly what's wrong!
-
diff --git a/docs/archive/V3_INTERFACE_README.md b/docs/archive/V3_INTERFACE_README.md
deleted file mode 100644
index 532c9e25..00000000
--- a/docs/archive/V3_INTERFACE_README.md
+++ /dev/null
@@ -1,231 +0,0 @@
-# LED Matrix Web Interface v3
-
-## Overview
-
-The v3 web interface is a complete rewrite of the LED Matrix control panel using modern web technologies for better performance, maintainability, and user experience. It uses Flask + HTMX + Alpine.js for a lightweight, server-side rendered interface with progressive enhancement.
-
-## 🚀 Key Features
-
-### Architecture
-- **HTMX** for dynamic content loading without full page reloads
-- **Alpine.js** for reactive components and state management
-- **SSE (Server-Sent Events)** for real-time updates
-- **Modular design** with blueprints for better code organization
-- **Progressive enhancement** - works without JavaScript
-
-### User Interface
-- **Modern, responsive design** with Tailwind CSS utility classes
-- **Tab-based navigation** for easy access to different features
-- **Real-time updates** for system stats, logs, and display preview
-- **Modal dialogs** for configuration and plugin management
-- **Drag-and-drop** font upload with progress indicators
-
-## 📋 Implemented Features
-
-### ✅ Complete Modules
-1. **Overview** - System stats, quick actions, display preview
-2. **General Settings** - Timezone, location, autostart configuration
-3. **Display Settings** - Hardware configuration, brightness, options
-4. **Durations** - Display rotation timing configuration
-5. **Sports Configuration** - Per-league settings with on-demand modes
-6. **Plugin Management** - Install, configure, enable/disable plugins
-7. **Font Management** - Upload fonts, manage overrides, preview
-8. **Logs Viewer** - Real-time log streaming with filtering and search
-
-### 🎯 Key Improvements Over v1/v2
-
-- **Modular Architecture**: Each tab loads independently via HTMX
-- **Real-time Updates**: SSE streams for live stats and logs
-- **Better Error Handling**: Consistent API responses and user feedback
-- **Enhanced UX**: Loading states, progress indicators, notifications
-- **Schema-driven Forms**: Dynamic form generation from JSON schemas
-- **Responsive Design**: Works well on different screen sizes
-- **Performance**: Server-side rendering with minimal JavaScript
-
-## 🛠️ Technical Stack
-
-### Backend
-- **Flask** with Blueprints for modular organization
-- **Jinja2** templates for server-side rendering
-- **SSE** for real-time data streaming
-- **Consistent API** with JSON envelope responses
-
-### Frontend
-- **HTMX** for AJAX interactions without writing JavaScript
-- **Alpine.js** for reactive state management
-- **Tailwind CSS** utility classes for styling
-- **Font Awesome** for icons
-
-## 🚦 Getting Started
-
-### Prerequisites
-- Python 3.7+
-- Flask
-- LED Matrix project setup
-
-### Running the Interface
-
-1. **Start the v3 interface**:
- ```bash
- python3 web_interface/start.py
- # Or use the shell script:
- ./web_interface/run.sh
- ```
-
-2. **Access the interface**:
- - Open `http://localhost:5000` in your browser
- - The interface will load with real-time system stats
-
-3. **Test functionality**:
- ```bash
- python test_v3_interface.py
- ```
-
-### Navigation
-
-- **Overview**: System stats, quick actions, display preview
-- **General**: Basic settings (timezone, location, autostart)
-- **Display**: Hardware configuration (rows, columns, brightness)
-- **Sports**: Per-league configuration with on-demand modes
-- **Plugins**: Plugin management and store
-- **Fonts**: Font upload, overrides, and preview
-- **Logs**: Real-time log viewer with filtering
-
-## 🔧 API Endpoints
-
-### Core Endpoints
-- `GET /` - Main interface (serves v3)
-- `GET /v3` - v3 interface (backwards compatibility)
-
-### API v3 Endpoints
-- `GET /api/v3/config/main` - Get main configuration
-- `POST /api/v3/config/main` - Save main configuration
-- `GET /api/v3/system/status` - Get system status
-- `POST /api/v3/system/action` - Execute system actions
-- `GET /api/v3/plugins/installed` - Get installed plugins
-- `GET /api/v3/fonts/catalog` - Get font catalog
-
-### SSE Streams
-- `/api/v3/stream/stats` - Real-time system stats
-- `/api/v3/stream/display` - Display preview updates
-- `/api/v3/stream/logs` - Real-time log streaming
-
-## 📁 File Structure
-
-```
-LEDMatrix/
-├── web_interface/ # Web interface package
-│ ├── __init__.py
-│ ├── app.py # Main Flask app with blueprints
-│ ├── start.py # Startup script
-│ ├── run.sh # Shell runner
-│ ├── requirements.txt # Dependencies
-│ ├── README.md # Web interface documentation
-│ ├── blueprints/
-│ │ ├── __init__.py
-│ │ ├── pages_v3.py # HTML pages and partials
-│ │ └── api_v3.py # API endpoints
-│ ├── templates/v3/
-│ │ ├── base.html # Main layout template
-│ │ ├── index.html # Overview page
-│ │ └── partials/ # HTMX partials
-│ │ ├── overview.html
-│ │ ├── general.html
-│ │ ├── display.html
-│ │ ├── sports.html
-│ │ ├── plugins.html
-│ │ ├── fonts.html
-│ │ └── logs.html
-│ └── static/v3/
-│ ├── app.css # Custom styles
-│ └── app.js # JavaScript helpers
-├── old_web_interface/ # Legacy v1/v2 (for reference)
-├── start_web_conditionally.py # Service starter
-└── test_v3_interface.py # Test script
-```
-
-## 🔄 Migration from v1/v2
-
-### What Changed
-- **Default Route**: `/` now serves v3 interface (was v1)
-- **API Prefix**: All v3 APIs use `/api/v3/` prefix
-- **SSE Streams**: New real-time update mechanism
-- **Modular Design**: Tabs load independently via HTMX
-
-### Backwards Compatibility
-- Old `/` route redirects to `/v3`
-- Original v1 interface still accessible via other routes
-- All existing functionality preserved in new structure
-
-### Migration Path
-1. **Phase 1-7**: Implement all v3 features ✅
-2. **Phase 8**: Update default route to v3 ✅
-3. **Testing**: Run comprehensive tests ✅
-4. **Cutover**: v3 becomes default interface ✅
-
-## 🧪 Testing
-
-### Automated Tests
-```bash
-python test_v3_interface.py
-```
-
-Tests cover:
-- Basic connectivity and routing
-- API endpoint accessibility
-- SSE stream functionality
-- HTMX partial loading
-- Form submissions
-- Configuration saving
-
-### Manual Testing Checklist
-
-- [ ] Navigate between all tabs
-- [ ] Test form submissions (General, Display, Sports)
-- [ ] Verify real-time updates (stats, logs)
-- [ ] Test plugin management (enable/disable)
-- [ ] Upload a font file
-- [ ] Test responsive design on mobile
-- [ ] Verify error handling for invalid inputs
-
-## 🚨 Known Limitations
-
-### Current Implementation
-- **Sample Data**: Many endpoints return sample data for testing
-- **No Real Integration**: Backend doesn't fully integrate with actual services yet
-- **Basic Error Handling**: Could be more comprehensive
-- **No Authentication**: Assumes local/trusted network
-
-### Production Readiness
-- **Security**: Add authentication and CSRF protection
-- **Performance**: Optimize for high traffic
-- **Monitoring**: Add proper logging and metrics
-- **Integration**: Connect to real LED matrix hardware/services
-
-## 🔮 Future Enhancements
-
-### Planned Features
-- **Advanced Editor**: Visual layout editor for display elements
-- **Plugin Store Integration**: Real plugin discovery and installation
-- **Advanced Analytics**: Usage metrics and performance monitoring
-- **Mobile App**: Companion mobile app for remote control
-
-### Technical Improvements
-- **WebSockets**: Replace SSE for bidirectional communication
-- **Caching**: Add Redis or similar for better performance
-- **API Rate Limiting**: Protect against abuse
-- **Database Integration**: Move from file-based config
-
-## 📞 Support
-
-For issues or questions:
-1. Run the test script: `python test_v3_interface.py`
-2. Check the logs tab for real-time debugging
-3. Review the browser console for JavaScript errors
-4. File issues in the project repository
-
----
-
-**Status**: ⚠️ **UI framework complete; integration and production hardening required (not production-ready)**
-
-The v3 interface UI and layout are finished, providing a modern, maintainable foundation for LED Matrix control. However, real service integration, authentication, security hardening, and monitoring remain to be implemented before production use.
diff --git a/docs/archive/VEGAS_SCROLL_MODE.md b/docs/archive/VEGAS_SCROLL_MODE.md
deleted file mode 100644
index 595699f4..00000000
--- a/docs/archive/VEGAS_SCROLL_MODE.md
+++ /dev/null
@@ -1,388 +0,0 @@
-# Vegas Scroll Mode - Plugin Developer Guide
-
-Vegas scroll mode displays content from multiple plugins in a continuous horizontal scroll, similar to the news tickers seen in Las Vegas casinos. This guide explains how to integrate your plugin with Vegas mode.
-
-## Overview
-
-When Vegas mode is enabled, the display controller composes content from all enabled plugins into a single continuous scroll. Each plugin can control how its content appears in the scroll using one of three **display modes**:
-
-| Mode | Behavior | Best For |
-|------|----------|----------|
-| **SCROLL** | Content scrolls continuously within the stream | Multi-item plugins (sports scores, odds, news) |
-| **FIXED_SEGMENT** | Fixed-width block that scrolls by | Static info (clock, weather, current temp) |
-| **STATIC** | Scroll pauses, plugin displays for duration, then resumes | Important alerts, detailed views |
-
-## Quick Start
-
-### Minimal Integration (Zero Code Changes)
-
-If you do nothing, your plugin will work with Vegas mode using these defaults:
-
-- Plugins with `get_vegas_content_type() == 'multi'` use **SCROLL** mode
-- Plugins with `get_vegas_content_type() == 'static'` use **FIXED_SEGMENT** mode
-- Content is captured by calling your plugin's `display()` method
-
-### Basic Integration
-
-To provide optimized Vegas content, implement `get_vegas_content()`:
-
-```python
-from PIL import Image
-
-class MyPlugin(BasePlugin):
- def get_vegas_content(self):
- """Return content for Vegas scroll mode."""
- # Return a single image for fixed content
- return self._render_current_view()
-
- # OR return multiple images for multi-item content
- # return [self._render_item(item) for item in self.items]
-```
-
-### Full Integration
-
-For complete control over Vegas behavior, implement these methods:
-
-```python
-from src.plugin_system.base_plugin import BasePlugin, VegasDisplayMode
-
-class MyPlugin(BasePlugin):
- def get_vegas_content_type(self) -> str:
- """Legacy method - determines default mode mapping."""
- return 'multi' # or 'static' or 'none'
-
- def get_vegas_display_mode(self) -> VegasDisplayMode:
- """Specify how this plugin behaves in Vegas scroll."""
- return VegasDisplayMode.SCROLL
-
- def get_supported_vegas_modes(self) -> list:
- """Return list of modes users can configure."""
- return [VegasDisplayMode.SCROLL, VegasDisplayMode.FIXED_SEGMENT]
-
- def get_vegas_content(self):
- """Return PIL Image(s) for the scroll."""
- return [self._render_game(g) for g in self.games]
-
- def get_vegas_segment_width(self) -> int:
- """For FIXED_SEGMENT: width in panels (optional)."""
- return 2 # Use 2 panels width
-```
-
-## Display Modes Explained
-
-### SCROLL Mode
-
-Content scrolls continuously within the Vegas stream. Best for plugins with multiple items.
-
-```python
-def get_vegas_display_mode(self):
- return VegasDisplayMode.SCROLL
-
-def get_vegas_content(self):
- # Return list of images - each scrolls individually
- images = []
- for game in self.games:
- img = Image.new('RGB', (200, 32))
- # ... render game info ...
- images.append(img)
- return images
-```
-
-**When to use:**
-- Sports scores with multiple games
-- Stock/odds tickers with multiple items
-- News feeds with multiple headlines
-
-### FIXED_SEGMENT Mode
-
-Content is rendered as a fixed-width block that scrolls by with other content.
-
-```python
-def get_vegas_display_mode(self):
- return VegasDisplayMode.FIXED_SEGMENT
-
-def get_vegas_content(self):
- # Return single image at your preferred width
- img = Image.new('RGB', (128, 32)) # 2 panels wide
- # ... render clock/weather/etc ...
- return img
-
-def get_vegas_segment_width(self):
- # Optional: specify width in panels
- return 2
-```
-
-**When to use:**
-- Clock display
-- Current weather/temperature
-- System status indicators
-- Any "at a glance" information
-
-### STATIC Mode
-
-Scroll pauses completely, your plugin displays using its normal `display()` method for its configured duration, then scroll resumes.
-
-```python
-def get_vegas_display_mode(self):
- return VegasDisplayMode.STATIC
-
-def get_display_duration(self):
- # How long to pause and show this plugin
- return 10.0 # 10 seconds
-```
-
-**When to use:**
-- Important alerts that need attention
-- Detailed information that's hard to read while scrolling
-- Interactive or animated content
-- Content that requires the full display
-
-## User Configuration
-
-Users can override the default display mode per-plugin in their config:
-
-```json
-{
- "my_plugin": {
- "enabled": true,
- "vegas_mode": "static", // Override: "scroll", "fixed", or "static"
- "vegas_panel_count": 2, // Width in panels for fixed mode
- "display_duration": 10 // Duration for static mode
- }
-}
-```
-
-The `get_vegas_display_mode()` method checks config first, then falls back to your implementation.
-
-## Content Rendering Guidelines
-
-### Image Dimensions
-
-- **Height**: Must match display height (typically 32 pixels)
-- **Width**:
- - SCROLL: Any width, content will scroll
- - FIXED_SEGMENT: `panels × single_panel_width` (e.g., 2 × 64 = 128px)
-
-### Color Mode
-
-Always use RGB mode for images:
-
-```python
-img = Image.new('RGB', (width, 32), color=(0, 0, 0))
-```
-
-### Performance Tips
-
-1. **Cache rendered images** - Don't re-render on every call
-2. **Pre-render on update()** - Render images when data changes, not when Vegas requests them
-3. **Keep images small** - Memory adds up with multiple plugins
-
-```python
-class MyPlugin(BasePlugin):
- def __init__(self, ...):
- super().__init__(...)
- self._cached_vegas_images = None
- self._cache_valid = False
-
- def update(self):
- # Fetch new data
- self.data = self._fetch_data()
- # Invalidate cache so next Vegas request re-renders
- self._cache_valid = False
-
- def get_vegas_content(self):
- if not self._cache_valid:
- self._cached_vegas_images = self._render_all_items()
- self._cache_valid = True
- return self._cached_vegas_images
-```
-
-## Fallback Behavior
-
-If your plugin doesn't implement `get_vegas_content()`, Vegas mode will:
-
-1. Create a temporary canvas matching display dimensions
-2. Call your `display()` method
-3. Capture the resulting image
-4. Use that image in the scroll
-
-This works but is less efficient than providing native Vegas content.
-
-## Excluding from Vegas Mode
-
-To exclude your plugin from Vegas scroll entirely:
-
-```python
-def get_vegas_content_type(self):
- return 'none'
-```
-
-Or users can exclude via config:
-
-```json
-{
- "display": {
- "vegas_scroll": {
- "excluded_plugins": ["my_plugin"]
- }
- }
-}
-```
-
-## Complete Example
-
-Here's a complete example of a weather plugin with full Vegas integration:
-
-```python
-from PIL import Image, ImageDraw
-from src.plugin_system.base_plugin import BasePlugin, VegasDisplayMode
-
-class WeatherPlugin(BasePlugin):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.temperature = None
- self.conditions = None
- self._vegas_image = None
-
- def update(self):
- """Fetch weather data."""
- data = self._fetch_weather_api()
- self.temperature = data['temp']
- self.conditions = data['conditions']
- self._vegas_image = None # Invalidate cache
-
- def display(self, force_clear=False):
- """Standard display for normal rotation."""
- if force_clear:
- self.display_manager.clear()
-
- # Full weather display with details
- self.display_manager.draw_text(
- f"{self.temperature}°F",
- x=10, y=8, color=(255, 255, 255)
- )
- self.display_manager.draw_text(
- self.conditions,
- x=10, y=20, color=(200, 200, 200)
- )
- self.display_manager.update_display()
-
- # --- Vegas Mode Integration ---
-
- def get_vegas_content_type(self):
- """Legacy compatibility."""
- return 'static'
-
- def get_vegas_display_mode(self):
- """Use FIXED_SEGMENT for compact weather display."""
- # Allow user override via config
- return super().get_vegas_display_mode()
-
- def get_supported_vegas_modes(self):
- """Weather can work as fixed or static."""
- return [VegasDisplayMode.FIXED_SEGMENT, VegasDisplayMode.STATIC]
-
- def get_vegas_segment_width(self):
- """Weather needs 2 panels to show clearly."""
- return self.config.get('vegas_panel_count', 2)
-
- def get_vegas_content(self):
- """Render compact weather for Vegas scroll."""
- if self._vegas_image is not None:
- return self._vegas_image
-
- # Create compact display (2 panels = 128px typical)
- panel_width = 64 # From display.hardware.cols
- panels = self.get_vegas_segment_width() or 2
- width = panel_width * panels
- height = 32
-
- img = Image.new('RGB', (width, height), color=(0, 0, 40))
- draw = ImageDraw.Draw(img)
-
- # Draw compact weather
- temp_text = f"{self.temperature}°"
- draw.text((10, 8), temp_text, fill=(255, 255, 255))
- draw.text((60, 8), self.conditions[:10], fill=(200, 200, 200))
-
- self._vegas_image = img
- return img
-```
-
-## API Reference
-
-### VegasDisplayMode Enum
-
-```python
-from src.plugin_system.base_plugin import VegasDisplayMode
-
-VegasDisplayMode.SCROLL # "scroll" - continuous scrolling
-VegasDisplayMode.FIXED_SEGMENT # "fixed" - fixed block in scroll
-VegasDisplayMode.STATIC # "static" - pause scroll to display
-```
-
-### BasePlugin Vegas Methods
-
-| Method | Returns | Description |
-|--------|---------|-------------|
-| `get_vegas_content()` | `Image` or `List[Image]` or `None` | Content for Vegas scroll |
-| `get_vegas_content_type()` | `str` | Legacy: 'multi', 'static', or 'none' |
-| `get_vegas_display_mode()` | `VegasDisplayMode` | How plugin behaves in Vegas |
-| `get_supported_vegas_modes()` | `List[VegasDisplayMode]` | Modes available for user config |
-| `get_vegas_segment_width()` | `int` or `None` | Width in panels for FIXED_SEGMENT |
-
-### Configuration Options
-
-**Per-plugin config:**
-```json
-{
- "plugin_id": {
- "vegas_mode": "scroll|fixed|static",
- "vegas_panel_count": 2,
- "display_duration": 15
- }
-}
-```
-
-**Global Vegas config:**
-```json
-{
- "display": {
- "vegas_scroll": {
- "enabled": true,
- "scroll_speed": 50,
- "separator_width": 32,
- "plugin_order": ["clock", "weather", "sports"],
- "excluded_plugins": ["debug_plugin"],
- "target_fps": 125,
- "buffer_ahead": 2
- }
- }
-}
-```
-
-## Troubleshooting
-
-### Plugin not appearing in Vegas scroll
-
-1. Check `get_vegas_content_type()` doesn't return `'none'`
-2. Verify plugin is not in `excluded_plugins` list
-3. Ensure plugin is enabled
-
-### Content looks wrong in scroll
-
-1. Verify image height matches display height (32px typical)
-2. Check image mode is 'RGB'
-3. Test with `get_vegas_content()` returning a simple test image
-
-### STATIC mode not pausing
-
-1. Verify `get_vegas_display_mode()` returns `VegasDisplayMode.STATIC`
-2. Check user hasn't overridden with `vegas_mode` in config
-3. Ensure `display()` method works correctly
-
-### Performance issues
-
-1. Implement image caching in `get_vegas_content()`
-2. Pre-render images in `update()` instead of on-demand
-3. Reduce image dimensions if possible
diff --git a/docs/archive/WEATHER_TROUBLESHOOTING.md b/docs/archive/WEATHER_TROUBLESHOOTING.md
deleted file mode 100644
index a11a0607..00000000
--- a/docs/archive/WEATHER_TROUBLESHOOTING.md
+++ /dev/null
@@ -1,298 +0,0 @@
-# Weather Plugin Troubleshooting Guide
-
-## Quick Diagnosis
-
-Run the troubleshooting script on your Pi:
-
-```bash
-./troubleshoot_weather.sh
-```
-
-This will check:
-- Plugin installation
-- Configuration files
-- API key setup
-- Network connectivity
-- Cache status
-
-## Common Issues
-
-### 1. "No Weather Data" Message
-
-This appears when the weather plugin cannot fetch or access weather data.
-
-### 2. Missing or Invalid API Key
-
-**Symptoms:**
-- Plugin shows "No Weather Data"
-- Logs show "No valid OpenWeatherMap API key configured"
-- Plugin initialized but no data updates
-
-**Solution:**
-
-1. Get an API key from [OpenWeatherMap](https://openweathermap.org/api)
- - Sign up for a free account
- - Navigate to API Keys section
- - Generate a new API key
-
-2. Add API key to `config/config_secrets.json` (recommended):
- ```json
- {
- "ledmatrix-weather": {
- "api_key": "your_actual_api_key_here"
- }
- }
- ```
-
- OR add directly to `config/config.json`:
- ```json
- {
- "ledmatrix-weather": {
- "enabled": true,
- "api_key": "your_actual_api_key_here",
- "location_city": "Dallas",
- "location_state": "Texas",
- "location_country": "US"
- }
- }
- ```
-
-3. Restart the LEDMatrix service:
- ```bash
- sudo systemctl restart ledmatrix
- ```
-
-### 3. Plugin Not Enabled
-
-**Symptoms:**
-- Plugin doesn't appear in display rotation
-- No weather data displayed
-
-**Solution:**
-
-Check `config/config.json` and ensure the plugin is enabled:
-
-```json
-{
- "ledmatrix-weather": {
- "enabled": true,
- "display_duration": 30,
- ...
- }
-}
-```
-
-### 4. Network/API Connectivity Issues
-
-**Symptoms:**
-- Plugin shows "No Weather Data"
-- Logs show connection errors or timeouts
-
-**Solution:**
-
-1. Check internet connectivity:
- ```bash
- ping -c 4 api.openweathermap.org
- ```
-
-2. Check firewall settings (if applicable)
-
-3. Verify DNS resolution:
- ```bash
- nslookup api.openweathermap.org
- ```
-
-4. Test API directly:
- ```bash
- curl "https://api.openweathermap.org/data/2.5/weather?q=Dallas,TX,US&appid=YOUR_API_KEY&units=imperial"
- ```
-
-### 5. API Rate Limits Exceeded
-
-**Symptoms:**
-- Plugin worked before but now shows "No Weather Data"
-- Logs show HTTP 429 errors
-
-**Solution:**
-
-OpenWeatherMap free tier limits:
-- 1,000 API calls per day
-- 60 calls per minute
-
-Default plugin settings use ~48 calls/day (1800s = 30 min intervals).
-
-If exceeded:
-- Wait for quota reset (daily)
-- Increase `update_interval` in config (minimum 300s = 5 minutes)
-- Upgrade OpenWeatherMap plan
-
-### 6. Invalid Location Configuration
-
-**Symptoms:**
-- Plugin shows "No Weather Data"
-- Logs show geocoding errors
-
-**Solution:**
-
-Ensure location is correctly configured in `config/config.json`:
-
-```json
-{
- "ledmatrix-weather": {
- "location_city": "Dallas",
- "location_state": "Texas",
- "location_country": "US"
- }
-}
-```
-
-- Use proper city names
-- Include state for US cities to avoid ambiguity
-- Use ISO 3166-1 alpha-2 country codes (US, GB, CA, etc.)
-
-### 7. Stale Cache Data
-
-**Symptoms:**
-- Weather data not updating
-- Old data displayed
-
-**Solution:**
-
-Clear the cache:
-
-```bash
-# Find cache files
-find cache/ -name "*weather*" -type f
-
-# Remove cache files (plugin will fetch fresh data)
-rm cache/*weather*
-```
-
-### 8. Plugin Not Loading
-
-**Symptoms:**
-- Weather modes don't appear in available modes
-- Logs show plugin loading errors
-
-**Solution:**
-
-1. Check plugin directory exists:
- ```bash
- ls -la plugins/ledmatrix-weather/
- ```
-
-2. Verify manifest.json is valid:
- ```bash
- python3 -m json.tool plugins/ledmatrix-weather/manifest.json
- ```
-
-3. Check logs for specific errors:
- ```bash
- sudo journalctl -u ledmatrix -f | grep -i weather
- ```
-
-4. Verify plugin dependencies are installed:
- ```bash
- pip3 install -r plugins/ledmatrix-weather/requirements.txt
- ```
-
-## Checking Logs
-
-View real-time logs:
-
-```bash
-sudo journalctl -u ledmatrix -f
-```
-
-Filter for weather-related messages:
-
-```bash
-sudo journalctl -u ledmatrix -f | grep -i weather
-```
-
-View last 100 lines:
-
-```bash
-sudo journalctl -u ledmatrix -n 100 | grep -i weather
-```
-
-## Configuration Example
-
-Complete configuration in `config/config.json`:
-
-```json
-{
- "ledmatrix-weather": {
- "enabled": true,
- "display_duration": 30,
- "location_city": "Dallas",
- "location_state": "Texas",
- "location_country": "US",
- "units": "imperial",
- "update_interval": 1800,
- "show_current_weather": true,
- "show_hourly_forecast": true,
- "show_daily_forecast": true,
- "transition": {
- "type": "redraw",
- "speed": 2,
- "enabled": true
- }
- }
-}
-```
-
-And in `config/config_secrets.json`:
-
-```json
-{
- "ledmatrix-weather": {
- "api_key": "your_openweathermap_api_key_here"
- }
-}
-```
-
-## Plugin Configuration Schema
-
-The plugin expects configuration under either:
-- `ledmatrix-weather` (plugin ID from manifest)
-- `weather` (legacy/deprecated)
-
-The system checks both when loading configuration.
-
-## Testing the Plugin
-
-1. Enable the plugin in config
-2. Restart the service: `sudo systemctl restart ledmatrix`
-3. Check logs: `sudo journalctl -u ledmatrix -f`
-4. Wait for update interval (default 30 minutes) or force update
-5. Check if weather modes appear in display rotation
-
-## Still Having Issues?
-
-1. Run the troubleshooting script: `./troubleshoot_weather.sh`
-2. Check service status: `sudo systemctl status ledmatrix`
-3. Review logs for specific error messages
-4. Verify all configuration files are valid JSON
-5. Ensure file permissions are correct:
- ```bash
- ls -la config/config.json config/config_secrets.json
- ```
-
-## API Key Security
-
-**Recommended:** Store API key in `config/config_secrets.json` with restricted permissions:
-
-```bash
-chmod 640 config/config_secrets.json
-```
-
-This file is not tracked by git (should be in .gitignore).
-
-## Plugin ID Note
-
-The weather plugin ID is `ledmatrix-weather` (from manifest.json). Configuration should use this ID, though the system also checks for `weather` for backward compatibility.
-
-
-
-
diff --git a/docs/archive/WEB_INTERFACE_TROUBLESHOOTING.md b/docs/archive/WEB_INTERFACE_TROUBLESHOOTING.md
deleted file mode 100644
index a14d04b0..00000000
--- a/docs/archive/WEB_INTERFACE_TROUBLESHOOTING.md
+++ /dev/null
@@ -1,314 +0,0 @@
-# Web Interface Troubleshooting Guide
-
-## Quick Diagnosis Steps
-
-Since the web interface doesn't seem to run and shows no logging after reorganization, follow these steps **on your Raspberry Pi** to diagnose the issue:
-
-### 1. Check Service Status
-
-```bash
-# Check if the web service is running
-sudo systemctl status ledmatrix-web
-
-# Check if it's enabled to start on boot
-sudo systemctl is-enabled ledmatrix-web
-```
-
-### 2. View Service Logs
-
-The service logs to **syslog**, not stdout. Use these commands to view logs:
-
-```bash
-# View recent web interface logs
-sudo journalctl -u ledmatrix-web -n 50 --no-pager
-
-# Follow logs in real-time
-sudo journalctl -u ledmatrix-web -f
-
-# View logs since last boot
-sudo journalctl -u ledmatrix-web -b
-```
-
-### 3. Check Configuration
-
-```bash
-# Check if web_display_autostart is enabled in config
-cat ~/LEDMatrix/config/config.json | grep web_display_autostart
-
-# Should show: "web_display_autostart": true
-```
-
-If it shows `false` or is missing, the web interface won't start (by design).
-
-### 4. Test Manual Startup
-
-Try starting the web interface manually to see error messages:
-
-```bash
-cd ~/LEDMatrix
-python3 web_interface/start.py
-```
-
-This will show any import errors or startup issues directly in the terminal.
-
-## Common Issues and Solutions
-
-### Issue 1: Service Not Running
-
-**Symptom:** `systemctl status ledmatrix-web` shows "inactive (dead)"
-
-**Solutions:**
-```bash
-# Start the service
-sudo systemctl start ledmatrix-web
-
-# Enable it to start on boot
-sudo systemctl enable ledmatrix-web
-
-# Check status again
-sudo systemctl status ledmatrix-web
-```
-
-### Issue 2: web_display_autostart is False
-
-**Symptom:** Service starts but immediately exits gracefully
-
-**Solution:**
-```bash
-# Edit config.json
-nano ~/LEDMatrix/config/config.json
-
-# Set web_display_autostart to true:
-"web_display_autostart": true
-
-# Restart the service
-sudo systemctl restart ledmatrix-web
-```
-
-### Issue 3: Import Errors
-
-**Symptom:** Service fails immediately with import errors in logs
-
-**Possible causes:**
-- Missing dependencies
-- Python path issues
-- Circular import problems
-
-**Solutions:**
-
-```bash
-# Install/reinstall web dependencies
-cd ~/LEDMatrix
-pip3 install --break-system-packages -r web_interface/requirements.txt
-
-# Check for Python errors
-python3 -c "from web_interface.app import app; print('OK')"
-```
-
-### Issue 4: Port Already in Use
-
-**Symptom:** Error message about port 5000 being in use
-
-**Solution:**
-```bash
-# Check what's using port 5000
-sudo lsof -i :5000
-
-# Kill the process if needed
-sudo kill -9
-
-# Or change the port in web_interface/start.py
-```
-
-### Issue 5: Permission Issues
-
-**Symptom:** Permission denied errors in logs
-
-**Solution:**
-```bash
-# Ensure proper ownership
-cd ~/LEDMatrix
-sudo chown -R ledpi:ledpi .
-
-# Restart service
-sudo systemctl restart ledmatrix-web
-```
-
-### Issue 6: Flask/Blueprint Import Errors
-
-**Symptom:** ImportError or ModuleNotFoundError in logs
-
-**Check these files exist:**
-```bash
-ls -la ~/LEDMatrix/web_interface/app.py
-ls -la ~/LEDMatrix/web_interface/start.py
-ls -la ~/LEDMatrix/web_interface/blueprints/api_v3.py
-ls -la ~/LEDMatrix/web_interface/blueprints/pages_v3.py
-```
-
-If any are missing, you may need to restore from git or the reorganization.
-
-## Detailed Logging Commands
-
-### View All Web Service Logs
-```bash
-# Show all logs with timestamps
-sudo journalctl -u ledmatrix-web --no-pager
-
-# Show logs from the last hour
-sudo journalctl -u ledmatrix-web --since "1 hour ago"
-
-# Show logs between specific times
-sudo journalctl -u ledmatrix-web --since "2024-10-14 10:00:00" --until "2024-10-14 11:00:00"
-
-# Show only errors
-sudo journalctl -u ledmatrix-web -p err
-```
-
-### Check Python Import Issues
-```bash
-cd ~/LEDMatrix
-
-# Test imports step by step
-python3 -c "import sys; sys.path.insert(0, '.'); from src.config_manager import ConfigManager; print('ConfigManager OK')"
-
-python3 -c "import sys; sys.path.insert(0, '.'); from src.plugin_system.plugin_manager import PluginManager; print('PluginManager OK')"
-
-python3 -c "import sys; sys.path.insert(0, '.'); from web_interface.app import app; print('Flask App OK')"
-```
-
-## Service File Check
-
-Verify the service file is correct:
-
-```bash
-cat /etc/systemd/system/ledmatrix-web.service
-```
-
-Should contain:
-```
-[Unit]
-Description=LED Matrix Web Interface Service
-After=network.target
-
-[Service]
-Type=simple
-User=root
-WorkingDirectory=/home/ledpi/LEDMatrix
-Environment=USE_THREADING=1
-ExecStart=/usr/bin/python3 /home/ledpi/LEDMatrix/start_web_conditionally.py
-Restart=on-failure
-RestartSec=10
-StandardOutput=syslog
-StandardError=syslog
-SyslogIdentifier=ledmatrix-web
-
-[Install]
-WantedBy=multi-user.target
-```
-
-If it's different or points to old paths, reinstall it:
-
-```bash
-cd ~/LEDMatrix
-sudo bash install_web_service.sh
-sudo systemctl daemon-reload
-sudo systemctl restart ledmatrix-web
-```
-
-## Post-Reorganization Checklist
-
-Verify the reorganization completed correctly:
-
-```bash
-cd ~/LEDMatrix
-
-# These files should exist in new locations:
-ls web_interface/app.py
-ls web_interface/start.py
-ls web_interface/requirements.txt
-ls web_interface/blueprints/api_v3.py
-ls web_interface/blueprints/pages_v3.py
-
-# start_web_conditionally.py should point to new location
-grep "web_interface/start.py" start_web_conditionally.py
-```
-
-## Emergency Recovery
-
-If nothing works, you can rollback to the old structure:
-
-```bash
-cd ~/LEDMatrix
-
-# Check git status
-git status
-
-# If changes aren't committed, revert
-git checkout .
-
-# Or restore specific files from old_web_interface
-# (if that directory exists)
-```
-
-## Recommended Diagnostic Sequence
-
-Run these commands in order to get a complete picture:
-
-```bash
-#!/bin/bash
-echo "=== Web Interface Diagnostic Report ==="
-echo ""
-echo "1. Service Status:"
-sudo systemctl status ledmatrix-web
-echo ""
-echo "2. Config Autostart Setting:"
-cat ~/LEDMatrix/config/config.json | grep web_display_autostart
-echo ""
-echo "3. Recent Logs (last 20 lines):"
-sudo journalctl -u ledmatrix-web -n 20 --no-pager
-echo ""
-echo "4. File Structure Check:"
-ls -la ~/LEDMatrix/web_interface/
-echo ""
-echo "5. Python Import Test:"
-cd ~/LEDMatrix
-python3 -c "from web_interface.app import app; print('✓ Flask app imports successfully')" 2>&1
-echo ""
-echo "=== End of Diagnostic Report ==="
-```
-
-Save this as `diagnose_web.sh`, make it executable, and run it:
-
-```bash
-chmod +x diagnose_web.sh
-./diagnose_web.sh
-```
-
-## Success Indicators
-
-When the web interface is running correctly, you should see:
-
-1. **Service Status:** "active (running)" in green
-2. **Logs:** "Starting LED Matrix Web Interface V3..."
-3. **Network:** Accessible at `http://:5000`
-4. **Process:** Python process listening on port 5000
-
-```bash
-# Check if it's listening
-sudo netstat -tlnp | grep :5000
-# or
-sudo ss -tlnp | grep :5000
-```
-
-## Contact/Help
-
-If you've tried all these steps and it still doesn't work, collect the following information:
-
-1. Output from the diagnostic script above
-2. Full service logs: `sudo journalctl -u ledmatrix-web -n 100 --no-pager`
-3. Output from manual startup attempt
-4. Git status and recent commits
-
-This will help identify the exact issue.
-
diff --git a/docs/archive/WEB_UI_RELIABILITY_IMPROVEMENTS.md b/docs/archive/WEB_UI_RELIABILITY_IMPROVEMENTS.md
deleted file mode 100644
index 3e0d1244..00000000
--- a/docs/archive/WEB_UI_RELIABILITY_IMPROVEMENTS.md
+++ /dev/null
@@ -1,405 +0,0 @@
-# Web UI Reliability Improvements - Implementation Summary
-
-This document summarizes the comprehensive reliability and maintainability improvements implemented for the web UI's plugin and configuration management.
-
-## Overview
-
-The implementation follows a four-phase approach, building foundational reliability infrastructure first, then adding state management, frontend improvements, and finally testing/monitoring capabilities.
-
-## Phase 1: Foundation & Reliability Layer ✅
-
-### 1.1 Atomic Configuration Saves
-
-**Files Created:**
-- `src/config_manager_atomic.py` - Atomic config save manager with backup/rollback
-- Enhanced `src/config_manager.py` - Added atomic save methods
-
-**Features:**
-- Atomic file writes (write to temp → validate → atomic move)
-- Automatic backups before saves (keeps last 5 backups)
-- Rollback functionality to restore from backups
-- Post-write validation with automatic rollback on failure
-- Handles both main config and secrets files atomically
-
-**Usage:**
-```python
-from src.config_manager import ConfigManager
-
-config_manager = ConfigManager()
-
-# Atomic save with backup
-result = config_manager.save_config_atomic(new_config, create_backup=True)
-
-# Rollback to previous version
-config_manager.rollback_config()
-
-# List available backups
-backups = config_manager.list_backups()
-```
-
-### 1.2 Plugin Operation Queue
-
-**Files Created:**
-- `src/plugin_system/operation_types.py` - Operation type definitions
-- `src/plugin_system/operation_queue.py` - Operation queue manager
-
-**Features:**
-- Serializes plugin operations (install, update, uninstall, enable, disable)
-- Prevents concurrent operations on same plugin
-- Operation status/progress tracking
-- Operation cancellation support
-- Operation history persistence
-
-**Usage:**
-```python
-from src.plugin_system.operation_queue import PluginOperationQueue
-from src.plugin_system.operation_types import OperationType
-
-queue = PluginOperationQueue()
-
-# Enqueue operation
-operation_id = queue.enqueue_operation(
- OperationType.INSTALL,
- "plugin-id",
- operation_callback=lambda op: install_plugin(op.plugin_id)
-)
-
-# Check status
-status = queue.get_operation_status(operation_id)
-
-# Cancel operation
-queue.cancel_operation(operation_id)
-```
-
-### 1.3 Structured Error Handling
-
-**Files Created:**
-- `src/web_interface/errors.py` - Error codes and structured error classes
-- `src/web_interface/error_handler.py` - Centralized error handling
-
-**Features:**
-- Error codes and categories (ConfigError, PluginError, ValidationError, etc.)
-- Consistent error response format
-- Error context (operation, plugin_id, config_key, etc.)
-- Suggested fixes in error responses
-- Structured error logging
-
-**Usage:**
-```python
-from src.web_interface.error_handler import handle_errors, create_error_response
-from src.web_interface.errors import ErrorCode
-
-@handle_errors()
-def my_endpoint():
- # Errors automatically converted to structured format
- pass
-
-# Manual error response
-return create_error_response(
- ErrorCode.PLUGIN_NOT_FOUND,
- "Plugin not found",
- context={"plugin_id": "test-plugin"}
-)
-```
-
-### 1.4 Health Monitoring
-
-**Files Created:**
-- `src/plugin_system/health_monitor.py` - Enhanced health monitoring
-
-**Features:**
-- Background health checks
-- Health status determination (healthy/degraded/unhealthy)
-- Health metrics aggregation
-- Auto-recovery suggestions based on health status
-
-**Usage:**
-```python
-from src.plugin_system.health_monitor import PluginHealthMonitor
-
-monitor = PluginHealthMonitor(health_tracker)
-monitor.start_monitoring()
-
-# Get health status
-status = monitor.get_plugin_health_status("plugin-id")
-
-# Get comprehensive metrics
-metrics = monitor.get_plugin_health_metrics("plugin-id")
-```
-
-## Phase 2: State Management & Synchronization ✅
-
-### 2.1 Centralized Plugin State Management
-
-**Files Created:**
-- `src/plugin_system/state_manager.py` - Centralized state manager
-
-**Features:**
-- Single source of truth for plugin state
-- State change events/notifications
-- State persistence to disk
-- State versioning for corruption detection
-
-**Usage:**
-```python
-from src.plugin_system.state_manager import PluginStateManager
-
-state_manager = PluginStateManager(state_file="plugin_state.json")
-
-# Update state
-state_manager.update_plugin_state("plugin-id", {
- "enabled": True,
- "version": "1.0.0"
-})
-
-# Subscribe to changes
-state_manager.subscribe_to_state_changes(
- callback=lambda plugin_id, old_state, new_state: print(f"{plugin_id} changed")
-)
-```
-
-### 2.2 State Reconciliation
-
-**Files Created:**
-- `src/plugin_system/state_reconciliation.py` - State reconciliation system
-
-**Features:**
-- Detects inconsistencies between config, manager, disk, and state manager
-- Auto-fixes safe inconsistencies
-- Flags dangerous inconsistencies for manual review
-- Comprehensive reconciliation reports
-
-**Usage:**
-```python
-from src.plugin_system.state_reconciliation import StateReconciliation
-
-reconciler = StateReconciliation(
- state_manager, config_manager, plugin_manager, plugins_dir
-)
-
-# Run reconciliation
-result = reconciler.reconcile_state()
-
-print(f"Found {len(result.inconsistencies_found)} inconsistencies")
-print(f"Fixed {len(result.inconsistencies_fixed)} automatically")
-```
-
-### 2.3 API Response Standardization
-
-**Files Created:**
-- `src/web_interface/api_helpers.py` - Standardized API response helpers
-
-**Features:**
-- Consistent success/error response format
-- Request validation helpers
-- Response metadata (timing, version, etc.)
-
-**Usage:**
-```python
-from src.web_interface.api_helpers import success_response, error_response, validate_request_json
-
-# Success response
-return success_response(
- data={"plugins": [...]},
- message="Plugins loaded successfully"
-)
-
-# Error response
-return error_response(
- ErrorCode.PLUGIN_NOT_FOUND,
- "Plugin not found",
- status_code=404
-)
-
-# Request validation
-data, error = validate_request_json(['plugin_id'])
-if error:
- return error
-```
-
-## Phase 3: Frontend Refactoring & UX ✅
-
-### 3.1 Modularized JavaScript
-
-**Files Created:**
-- `web_interface/static/v3/js/plugins/api_client.js` - API communication
-- `web_interface/static/v3/js/plugins/store_manager.js` - Plugin store logic
-- `web_interface/static/v3/js/plugins/config_manager.js` - Config form management
-- `web_interface/static/v3/js/plugins/install_manager.js` - Install/update logic
-- `web_interface/static/v3/js/plugins/state_manager.js` - Frontend state management
-- `web_interface/static/v3/js/utils/error_handler.js` - Frontend error handling
-
-**Structure:**
-- Split 4400+ line file into logical modules
-- ES6 module pattern with proper exports
-- Clear module boundaries and responsibilities
-- Shared utilities for common operations
-
-**Usage:**
-```javascript
-// API calls
-const plugins = await PluginAPI.getInstalledPlugins();
-await PluginAPI.togglePlugin("plugin-id", true);
-
-// Store management
-const storePlugins = await PluginStoreManager.loadStore();
-await PluginStoreManager.installPlugin("plugin-id");
-
-// State management
-await PluginStateManager.loadInstalledPlugins();
-PluginStateManager.setPluginEnabled("plugin-id", true);
-
-// Error handling
-errorHandler.displayError(error, "Failed to install plugin");
-```
-
-### 3.2 Improved Error Messages
-
-**Features:**
-- User-friendly error formatting
-- Contextual help and suggestions
-- Copy error details functionality
-- Links to troubleshooting docs
-
-### 3.3 Configuration UI Enhancements
-
-**Features:**
-- Real-time validation feedback
-- Config diff viewer (structure in place)
-- Config export/import (structure in place)
-- Config templates/presets (structure in place)
-
-## Phase 4: Testing & Monitoring ✅
-
-### 4.1 Testing Infrastructure
-
-**Files Created:**
-- `test/web_interface/test_config_manager_atomic.py` - Tests for atomic config saves
-- `test/web_interface/test_plugin_operation_queue.py` - Tests for operation queue
-- `test/web_interface/integration/` - Directory for integration tests
-
-**Coverage:**
-- Unit tests for atomic config saves
-- Unit tests for operation queue
-- Integration test structure
-
-### 4.2 Structured Logging
-
-**Files Created:**
-- `src/web_interface/logging_config.py` - Structured logging configuration
-
-**Features:**
-- JSON-formatted structured logging
-- Plugin operation logging with context
-- Config change logging with before/after values
-- Error context logging
-
-**Usage:**
-```python
-from src.web_interface.logging_config import (
- setup_structured_logging,
- log_plugin_operation,
- log_config_change
-)
-
-# Setup logging
-setup_structured_logging(use_json=True)
-
-# Log operations
-log_plugin_operation(
- logger,
- "install",
- "plugin-id",
- "success",
- context={"version": "1.0.0"}
-)
-
-# Log config changes
-log_config_change(
- logger,
- "plugin-id",
- "save",
- before=old_config,
- after=new_config
-)
-```
-
-### 4.3 Operation History & Audit Log
-
-**Files Created:**
-- `src/plugin_system/operation_history.py` - Operation history tracker
-
-**Features:**
-- Tracks all plugin operations
-- Tracks all config changes
-- Persistent storage
-- Filtering and querying
-
-**Usage:**
-```python
-from src.plugin_system.operation_history import OperationHistory
-
-history = OperationHistory(history_file="operation_history.json")
-
-# Record operation
-history.record_operation(
- "install",
- plugin_id="plugin-id",
- status="success",
- user="admin"
-)
-
-# Get history
-records = history.get_history(
- limit=50,
- plugin_id="plugin-id"
-)
-```
-
-## Integration Notes
-
-### Backward Compatibility
-
-All changes maintain backward compatibility:
-- Existing API endpoints continue to work
-- Old code can gradually migrate to new infrastructure
-- Feature flags can be added for gradual rollout
-
-### Migration Path
-
-1. **Phase 1** infrastructure is ready to use but not yet integrated into all endpoints
-2. **Phase 2** state management can be integrated incrementally
-3. **Phase 3** frontend modules are available but original file still works
-4. **Phase 4** testing and logging can be enabled gradually
-
-### Next Steps
-
-1. Integrate atomic config saves into existing save endpoints
-2. Integrate operation queue into plugin install/update/uninstall endpoints
-3. Use structured errors in all API endpoints
-4. Integrate state manager with plugin manager
-5. Migrate frontend code to use new modules
-6. Add integration tests for critical flows
-7. Enable structured logging in production
-
-## Benefits
-
-1. **Reliability**: Atomic saves prevent config corruption, operation queue prevents conflicts
-2. **Debuggability**: Structured errors and logging provide clear context
-3. **Maintainability**: Modular code is easier to understand and modify
-4. **Consistency**: Standardized APIs and error handling
-5. **Observability**: Health monitoring and operation history provide visibility
-
-## Testing
-
-Run tests with:
-```bash
-python -m pytest test/web_interface/
-```
-
-## Documentation
-
-- See individual module docstrings for detailed API documentation
-- Error codes are documented in `src/web_interface/errors.py`
-- Operation types are documented in `src/plugin_system/operation_types.py`
-
diff --git a/docs/archive/WIFI_ETHERNET_AP_MODE_FIX.md b/docs/archive/WIFI_ETHERNET_AP_MODE_FIX.md
deleted file mode 100644
index 4b128fdb..00000000
--- a/docs/archive/WIFI_ETHERNET_AP_MODE_FIX.md
+++ /dev/null
@@ -1,194 +0,0 @@
-# WiFi Monitor Ethernet Check Fix
-
-## Problem
-
-The WiFi monitor service was enabling Access Point (AP) mode whenever WiFi was disconnected, even when the Raspberry Pi was connected via Ethernet. This caused:
-
-- AP mode to activate unnecessarily when Ethernet was available
-- Potential network conflicts
-- Confusion for users with hardwired connections
-
-## Solution
-
-Updated the WiFi manager to check for Ethernet connectivity before enabling AP mode. AP mode will now only be enabled when:
-
-- **WiFi is NOT connected** AND
-- **Ethernet is NOT connected**
-
-## Changes Made
-
-### 1. Added Ethernet Detection Method
-
-Added `_is_ethernet_connected()` method to `src/wifi_manager.py` that:
-- Checks for active Ethernet interfaces (eth0, enp*, etc.)
-- Verifies the interface has an IP address
-- Uses `nmcli` if available, falls back to `ip` command
-- Returns `True` if Ethernet is connected and has an IP
-
-### 2. Updated AP Mode Enable Logic
-
-Modified `enable_ap_mode()` to:
-- Check for Ethernet connection before enabling AP mode
-- Return an error message if Ethernet is connected: "Cannot enable AP mode while Ethernet is connected"
-
-### 3. Updated AP Mode Management Logic
-
-Modified `check_and_manage_ap_mode()` to:
-- Check both WiFi and Ethernet status
-- Only enable AP mode if both are disconnected
-- Disable AP mode if either WiFi or Ethernet connects
-- Log appropriate messages for each scenario
-
-### 4. Enhanced Logging
-
-Updated `wifi_monitor_daemon.py` to:
-- Log Ethernet connection status
-- Include Ethernet status in state change detection
-- Log when AP mode is disabled due to Ethernet connection
-
-## Testing
-
-### Verify Ethernet Detection
-
-```bash
-# Check if Ethernet is detected
-python3 -c "
-from src.wifi_manager import WiFiManager
-wm = WiFiManager()
-print('Ethernet connected:', wm._is_ethernet_connected())
-"
-```
-
-### Test AP Mode Behavior
-
-1. **With Ethernet connected**:
- ```bash
- # AP mode should NOT enable
- sudo systemctl restart ledmatrix-wifi-monitor
- sudo journalctl -u ledmatrix-wifi-monitor -f
- # Should see: "Cannot enable AP mode while Ethernet is connected"
- ```
-
-2. **With Ethernet disconnected and WiFi disconnected**:
- ```bash
- # Disconnect Ethernet cable
- # AP mode SHOULD enable
- sudo journalctl -u ledmatrix-wifi-monitor -f
- # Should see: "Auto-enabled AP mode (no WiFi or Ethernet connection)"
- ```
-
-3. **With Ethernet connected and WiFi connects**:
- ```bash
- # Connect WiFi
- # AP mode should disable if it was active
- sudo journalctl -u ledmatrix-wifi-monitor -f
- # Should see: "Auto-disabled AP mode (WiFi connected)"
- ```
-
-4. **With Ethernet connects while AP is active**:
- ```bash
- # Connect Ethernet cable while AP mode is active
- # AP mode should disable
- sudo journalctl -u ledmatrix-wifi-monitor -f
- # Should see: "Auto-disabled AP mode (Ethernet connected)"
- ```
-
-## Deployment
-
-### On Existing Installations
-
-1. **Restart the WiFi monitor service**:
- ```bash
- sudo systemctl restart ledmatrix-wifi-monitor
- ```
-
-2. **If AP mode is currently active and Ethernet is connected**, it will automatically disable:
- ```bash
- # Check current status
- sudo systemctl status hostapd
-
- # The service should automatically disable AP mode within 30 seconds
- # Or manually disable:
- sudo systemctl stop hostapd dnsmasq
- ```
-
-3. **Verify the fix**:
- ```bash
- # Check logs
- sudo journalctl -u ledmatrix-wifi-monitor -n 20
-
- # Should see messages about Ethernet connection status
- ```
-
-### On New Installations
-
-The fix is included automatically - no additional steps needed.
-
-## Behavior Summary
-
-| WiFi Status | Ethernet Status | AP Mode | Reason |
-|------------|----------------|---------|--------|
-| Connected | Connected | ❌ Disabled | Both connections available |
-| Connected | Disconnected | ❌ Disabled | WiFi available |
-| Disconnected | Connected | ❌ Disabled | Ethernet available |
-| Disconnected | Disconnected | ✅ Enabled | No network connection |
-
-## Troubleshooting
-
-### AP Mode Still Enables with Ethernet Connected
-
-1. **Check Ethernet detection**:
- ```bash
- python3 -c "
- from src.wifi_manager import WiFiManager
- wm = WiFiManager()
- print('Ethernet connected:', wm._is_ethernet_connected())
- "
- ```
-
-2. **Check network interface status**:
- ```bash
- nmcli device status
- # OR
- ip addr show
- ```
-
-3. **Verify Ethernet has IP address**:
- ```bash
- ip addr show eth0
- # Should show an "inet" address (not just 127.0.0.1)
- ```
-
-### Ethernet Not Detected
-
-If Ethernet is connected but not detected:
-
-1. **Check interface name**:
- ```bash
- ip link show
- # Look for Ethernet interfaces (may be eth0, enp*, etc.)
- ```
-
-2. **Check NetworkManager status**:
- ```bash
- sudo systemctl status NetworkManager
- ```
-
-3. **Manually check interface**:
- ```bash
- nmcli device status | grep ethernet
- ```
-
-## Related Files
-
-- `src/wifi_manager.py` - Main WiFi management logic
-- `scripts/utils/wifi_monitor_daemon.py` - Background daemon that monitors WiFi/Ethernet
-- `scripts/install/install_wifi_monitor.sh` - Installation script for WiFi monitor service
-
-## Notes
-
-- The Ethernet check uses `nmcli` if available (preferred), otherwise falls back to `ip` command
-- The check verifies that the interface has an actual IP address (not just link up)
-- AP mode will automatically disable within 30 seconds (check interval) when Ethernet connects
-- Manual AP mode enable via web interface will also respect Ethernet connection status
-
diff --git a/docs/archive/WIFI_SETUP.md b/docs/archive/WIFI_SETUP.md
deleted file mode 100644
index 3415f829..00000000
--- a/docs/archive/WIFI_SETUP.md
+++ /dev/null
@@ -1,368 +0,0 @@
-# WiFi Setup Feature
-
-The LED Matrix project includes a WiFi setup feature that allows you to configure WiFi connections through a web interface. When the Raspberry Pi is not connected to WiFi, it automatically broadcasts an access point (AP) that you can connect to for initial setup.
-
-## Features
-
-- **Automatic AP Mode**: When no WiFi connection is detected, the Raspberry Pi automatically creates a WiFi access point named "LEDMatrix-Setup"
-- **Web Interface**: Access the WiFi setup interface through your web browser
-- **Network Scanning**: Scan for available WiFi networks from the web interface
-- **Secure Connection**: Save WiFi credentials securely
-- **Automatic Management**: The WiFi monitor daemon automatically enables/disables AP mode based on connection status
-
-## Requirements
-
-The following packages are required for WiFi setup functionality:
-
-- **hostapd**: Access point software
-- **dnsmasq**: DHCP server for AP mode
-- **NetworkManager** (or **iwlist**): WiFi management tools
-
-These packages are automatically checked and can be installed during the WiFi monitor service installation.
-
-## Installation
-
-### 1. Install WiFi Monitor Service
-
-Run the installation script to set up the WiFi monitor daemon:
-
-```bash
-cd /home/ledpi/LEDMatrix
-sudo ./scripts/install/install_wifi_monitor.sh
-```
-
-This script will:
-- Check for required packages and offer to install them
-- Create the systemd service file
-- Enable and start the WiFi monitor service
-- Configure the service to start on boot
-
-### 2. Verify Service Status
-
-Check that the WiFi monitor service is running:
-
-```bash
-sudo systemctl status ledmatrix-wifi-monitor
-```
-
-You should see output indicating the service is active and running.
-
-## Usage
-
-### Accessing the WiFi Setup Interface
-
-1. **If WiFi is NOT connected**: The Raspberry Pi will automatically create an access point (after a 90-second grace period)
- - Connect to the WiFi network: **LEDMatrix-Setup** (open network, no password required)
- - Open a web browser and navigate to: `http://192.168.4.1:5000` or `http://192.168.4.1` (captive portal may redirect)
- - Or use the IP address shown in the web interface
-
-2. **If WiFi IS connected**: Access the web interface normally
- - Navigate to: `http://:5000`
- - Click on the **WiFi** tab in the navigation
-
-### Connecting to a WiFi Network
-
-1. Navigate to the **WiFi** tab in the web interface
-2. Click **Scan** to search for available networks
-3. Select a network from the dropdown menu, or enter the SSID manually
-4. Enter the WiFi password (leave empty for open networks)
-5. Click **Connect**
-6. The system will attempt to connect to the selected network
-7. Once connected, AP mode will automatically disable
-
-### Manual AP Mode Control
-
-You can manually enable or disable AP mode from the web interface:
-
-- **Enable AP Mode**: Click "Enable AP Mode" button (only available when WiFi is not connected)
-- **Disable AP Mode**: Click "Disable AP Mode" button (only available when AP mode is active)
-
-## How It Works
-
-### WiFi Monitor Daemon
-
-The WiFi monitor daemon (`wifi_monitor_daemon.py`) runs as a background service that:
-
-1. Checks WiFi connection status every 30 seconds (configurable)
-2. Automatically enables AP mode only if:
- - `auto_enable_ap_mode` is enabled in config AND
- - No WiFi connection is detected AND
- - No Ethernet connection is detected
-3. Automatically disables AP mode when WiFi or Ethernet connection is established
-4. Logs all state changes for troubleshooting
-
-**Note**: By default, `auto_enable_ap_mode` is `true`, meaning AP mode will automatically activate when both WiFi and Ethernet are disconnected. However, there's a 90-second grace period (3 consecutive checks at 30-second intervals) to prevent AP mode from enabling on transient network hiccups. This ensures you can always configure the device even when it has no network connection.
-
-### WiFi Manager Module
-
-The WiFi manager (`src/wifi_manager.py`) provides:
-
-- **Connection Status**: Checks current WiFi connection state
-- **Network Scanning**: Scans for available WiFi networks
-- **Connection Management**: Connects to WiFi networks and saves credentials
-- **AP Mode Control**: Manages access point mode (hostapd/dnsmasq)
-
-### Configuration
-
-WiFi settings are stored in `config/wifi_config.json`:
-
-```json
-{
- "ap_ssid": "LEDMatrix-Setup",
- "ap_password": "ledmatrix123",
- "ap_channel": 7,
- "auto_enable_ap_mode": true,
- "saved_networks": [
- {
- "ssid": "MyNetwork",
- "password": "mypassword",
- "saved_at": 1234567890.0
- }
- ]
-}
-```
-
-**Configuration Options:**
-- `ap_ssid`: SSID for the access point (default: "LEDMatrix-Setup")
-- `ap_channel`: WiFi channel for AP mode (default: 7)
-- `auto_enable_ap_mode`: Automatically enable AP mode when WiFi/Ethernet disconnect (default: `true`)
- - When `true`: AP mode automatically enables after a 90-second grace period when both WiFi and Ethernet are disconnected
- - When `false`: AP mode must be manually enabled through the web interface
-- `saved_networks`: List of saved WiFi network credentials
-
-**Note**: The access point is configured as an open network (no password required) for ease of initial setup. This allows any device to connect without credentials.
-
-### Access Point Configuration
-
-The AP mode uses `hostapd` and `dnsmasq` for access point functionality:
-
-- **SSID**: LEDMatrix-Setup (configurable)
-- **IP Range**: 192.168.4.2 - 192.168.4.20
-- **Gateway**: 192.168.4.1
-- **Channel**: 7 (configurable)
-
-## Verification
-
-### Running the WiFi Verification Script
-
-Use the comprehensive verification script to check your WiFi setup:
-
-```bash
-cd /home/ledpi/LEDMatrix
-./scripts/verify_wifi_setup.sh
-```
-
-This script checks:
-- Required packages are installed
-- WiFi monitor service is running
-- Configuration files are valid
-- WiFi permissions are configured
-- WiFi interface is available
-- WiFi radio status
-- Current connection status
-- AP mode status
-- WiFi Manager module availability
-- Web interface API accessibility
-
-The script provides a summary with passed/warning/failed checks to help diagnose issues.
-
-## Troubleshooting
-
-### WiFi Monitor Service Not Starting
-
-Check the service logs:
-
-```bash
-sudo journalctl -u ledmatrix-wifi-monitor -n 50
-```
-
-Common issues:
-- Missing packages (hostapd, dnsmasq)
-- Permission issues
-- Network interface not available
-
-### Cannot Access AP Mode
-
-1. Check if AP mode is active:
- ```bash
- sudo systemctl status hostapd
- ```
-
-2. Check if dnsmasq is running:
- ```bash
- sudo systemctl status dnsmasq
- ```
-
-3. Verify WiFi interface exists:
- ```bash
- ip link show wlan0
- ```
-
-### Cannot Connect to WiFi Network
-
-1. Verify the SSID and password are correct
-2. Check if the network requires a password (some networks may appear open but require a password)
-3. Check WiFi monitor logs for connection errors:
- ```bash
- sudo journalctl -u ledmatrix-wifi-monitor -f
- ```
-
-4. Check NetworkManager logs:
- ```bash
- sudo journalctl -u NetworkManager -n 50
- ```
-
-### AP Mode Not Disabling
-
-If AP mode doesn't disable after connecting to WiFi:
-
-1. Check WiFi connection status:
- ```bash
- nmcli device status
- ```
-
-2. Manually disable AP mode from the web interface
-3. Restart the WiFi monitor service:
- ```bash
- sudo systemctl restart ledmatrix-wifi-monitor
- ```
-
-## Service Management
-
-### Useful Commands
-
-```bash
-# Check service status
-sudo systemctl status ledmatrix-wifi-monitor
-
-# Start the service
-sudo systemctl start ledmatrix-wifi-monitor
-
-# Stop the service
-sudo systemctl stop ledmatrix-wifi-monitor
-
-# Restart the service
-sudo systemctl restart ledmatrix-wifi-monitor
-
-# View logs
-sudo journalctl -u ledmatrix-wifi-monitor -f
-
-# Disable service from starting on boot
-sudo systemctl disable ledmatrix-wifi-monitor
-
-# Enable service to start on boot
-sudo systemctl enable ledmatrix-wifi-monitor
-```
-
-### Configuration Options
-
-You can modify the check interval by editing the service file:
-
-```bash
-sudo systemctl edit ledmatrix-wifi-monitor
-```
-
-Or modify the service file directly:
-
-```bash
-sudo nano /etc/systemd/system/ledmatrix-wifi-monitor.service
-```
-
-Change the `--interval` parameter in the `ExecStart` line (default is 30 seconds).
-
-After modifying, reload and restart:
-
-```bash
-sudo systemctl daemon-reload
-sudo systemctl restart ledmatrix-wifi-monitor
-```
-
-## Security Considerations
-
-- **Open AP Network**: The access point is configured as an open network (no password) for ease of initial setup. This allows any device within range to connect to the setup network. Consider your deployment environment when using this feature.
-- **WiFi Credentials**: Saved WiFi credentials are stored in `config/wifi_config.json`. Ensure proper file permissions:
- ```bash
- sudo chmod 600 config/wifi_config.json
- ```
-- **Network Access**: When in AP mode, anyone within range can connect to the setup network. This is by design to allow easy initial configuration. For production deployments in secure environments, consider using the web interface when connected to WiFi instead.
-
-## API Endpoints
-
-The WiFi setup feature exposes the following API endpoints:
-
-- `GET /api/v3/wifi/status` - Get current WiFi connection status
-- `GET /api/v3/wifi/scan` - Scan for available WiFi networks
-- `POST /api/v3/wifi/connect` - Connect to a WiFi network
-- `POST /api/v3/wifi/ap/enable` - Enable access point mode
-- `POST /api/v3/wifi/ap/disable` - Disable access point mode
-
-## Technical Details
-
-### WiFi Detection Methods
-
-The WiFi manager tries multiple methods to detect WiFi status:
-
-1. **NetworkManager (nmcli)** - Preferred method if available
-2. **iwconfig** - Fallback method for systems without NetworkManager
-
-### Network Scanning
-
-The system supports multiple scanning methods:
-
-1. **nmcli** - Fast, preferred method
-2. **iwlist** - Fallback method for older systems
-
-### Access Point Setup
-
-AP mode configuration:
-
-- Uses `hostapd` (preferred) or `nmcli hotspot` (fallback) for WiFi access point functionality
-- Uses `dnsmasq` for DHCP and DNS services (hostapd mode only)
-- Configures wlan0 interface in AP mode
-- Provides DHCP range: 192.168.4.2-20
-- Gateway IP: 192.168.4.1
-- **Open network**: No password required (configures as open network for easy setup)
-- Captive portal: DNS redirection for automatic browser redirects (hostapd mode only)
-
-## Development
-
-### Testing WiFi Manager
-
-You can test the WiFi manager directly:
-
-```python
-from src.wifi_manager import WiFiManager
-
-# Create WiFi manager instance
-wifi_manager = WiFiManager()
-
-# Get status
-status = wifi_manager.get_wifi_status()
-print(f"Connected: {status.connected}, SSID: {status.ssid}")
-
-# Scan networks
-networks = wifi_manager.scan_networks()
-for net in networks:
- print(f"{net.ssid}: {net.signal}% ({net.security})")
-
-# Connect to network
-success, message = wifi_manager.connect_to_network("MyNetwork", "password")
-print(f"Connection: {success}, Message: {message}")
-```
-
-### Running Monitor Daemon Manually
-
-For testing, you can run the daemon in foreground mode:
-
-```bash
-sudo python3 wifi_monitor_daemon.py --interval 10 --foreground
-```
-
-## Support
-
-For issues or questions:
-1. Check the logs: `sudo journalctl -u ledmatrix-wifi-monitor -f`
-2. Review this documentation
-3. Check the main project README for general troubleshooting
-4. Open an issue on GitHub if needed
-
diff --git a/docs/archive/WEB_UI_AUDIT_2026-09.md b/docs/audits/WEB_UI_AUDIT_2026-09.md
similarity index 100%
rename from docs/archive/WEB_UI_AUDIT_2026-09.md
rename to docs/audits/WEB_UI_AUDIT_2026-09.md
diff --git a/docs/plugin_registry_template.json b/docs/plugin_registry_template.json
index 2f50a43d..989a3005 100644
--- a/docs/plugin_registry_template.json
+++ b/docs/plugin_registry_template.json
@@ -1,7 +1,6 @@
{
"version": "1.0.0",
- "last_updated": "2025-01-09T12:00:00Z",
- "description": "Official plugin registry for LEDMatrix",
+ "last_updated": "2026-09-03",
"plugins": [
{
"id": "hello-world",
@@ -10,86 +9,32 @@
"author": "ChuckBuilds",
"category": "example",
"tags": ["example", "tutorial", "beginner"],
- "repo": "https://github.com/ChuckBuilds/LEDMatrix",
+ "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins",
"branch": "main",
- "path": "plugins/hello-world",
- "versions": [
- {
- "version": "1.0.0",
- "ledmatrix_min_version": "2.0.0",
- "released": "2025-01-09",
- "download_url": "https://github.com/ChuckBuilds/LEDMatrix/archive/refs/heads/main.zip",
- "changelog": "Initial release"
- }
- ],
+ "plugin_path": "plugins/hello-world",
"stars": 0,
"downloads": 0,
- "last_updated": "2025-01-09",
+ "last_updated": "2026-09-03",
"verified": true,
- "documentation": "https://github.com/ChuckBuilds/LEDMatrix/blob/main/plugins/hello-world/README.md"
+ "screenshot": "",
+ "latest_version": "1.0.0"
},
{
- "id": "clock-simple",
- "name": "Simple Clock",
- "description": "A clean, simple clock display with date and time",
- "author": "ChuckBuilds",
- "category": "time",
- "tags": ["clock", "time", "date"],
- "repo": "https://github.com/ChuckBuilds/LEDMatrix",
+ "id": "my-third-party-plugin",
+ "name": "My Third-Party Plugin",
+ "description": "A plugin kept in its own repository (empty plugin_path)",
+ "author": "YourName",
+ "category": "custom",
+ "tags": ["example"],
+ "repo": "https://github.com/YourName/ledmatrix-my-third-party-plugin",
"branch": "main",
- "path": "plugins/clock-simple",
- "versions": [
- {
- "version": "1.0.0",
- "ledmatrix_min_version": "2.0.0",
- "released": "2025-01-09",
- "download_url": "https://github.com/ChuckBuilds/LEDMatrix/archive/refs/heads/main.zip",
- "changelog": "Initial release"
- }
- ],
+ "plugin_path": "",
"stars": 0,
"downloads": 0,
- "last_updated": "2025-01-09",
- "verified": true,
- "documentation": "https://github.com/ChuckBuilds/LEDMatrix/blob/main/plugins/clock-simple/README.md"
- }
- ],
- "categories": [
- {
- "id": "time",
- "name": "Time & Clocks",
- "description": "Clock displays, timers, and time-related plugins"
- },
- {
- "id": "sports",
- "name": "Sports",
- "description": "Scoreboards, schedules, and sports statistics"
- },
- {
- "id": "weather",
- "name": "Weather",
- "description": "Weather forecasts and conditions"
- },
- {
- "id": "finance",
- "name": "Finance",
- "description": "Stock tickers, crypto, and market data"
- },
- {
- "id": "entertainment",
- "name": "Entertainment",
- "description": "Games, animations, and media displays"
- },
- {
- "id": "example",
- "name": "Examples & Tutorials",
- "description": "Example plugins for learning"
- },
- {
- "id": "custom",
- "name": "Custom",
- "description": "Unique and miscellaneous displays"
+ "last_updated": "2026-09-03",
+ "verified": false,
+ "screenshot": "",
+ "latest_version": "1.0.0"
}
]
}
-
diff --git a/scripts/README_NBA_LOGOS.md b/scripts/README_NBA_LOGOS.md
deleted file mode 100644
index 8ae9b496..00000000
--- a/scripts/README_NBA_LOGOS.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# NBA Logo Downloader
-
-This script downloads all NBA team logos from the ESPN API and saves
-them in the `assets/sports/nba_logos/` directory.
-
-> **Heads up:** the NBA leaderboard and basketball scoreboards now
-> live as plugins in the
-> [`ledmatrix-plugins`](https://github.com/ChuckBuilds/ledmatrix-plugins)
-> repo (`basketball-scoreboard`, `ledmatrix-leaderboard`). Those
-> plugins download the logos they need automatically on first display.
-> This standalone script is mainly useful when you want to pre-populate
-> the assets directory ahead of time, or for development/debugging.
-
-All commands below should be run from the LEDMatrix project root.
-
-## Usage
-
-### Basic Usage
-```bash
-python3 scripts/download_nba_logos.py
-```
-
-### Force Re-download
-If you want to re-download all logos (even if they already exist):
-```bash
-python3 scripts/download_nba_logos.py --force
-```
-
-### Quiet Mode
-Reduce logging output:
-```bash
-python3 scripts/download_nba_logos.py --quiet
-```
-
-### Combined Options
-```bash
-python3 scripts/download_nba_logos.py --force --quiet
-```
-
-## What It Does
-
-1. **Fetches NBA Team Data**: Gets the complete list of NBA teams from ESPN API
-2. **Downloads Logos**: Downloads each team's logo from ESPN's servers
-3. **Saves Locally**: Saves logos as `{team_abbr}.png` in `assets/sports/nba_logos/`
-4. **Skips Existing**: By default, skips teams that already have logos
-5. **Rate Limiting**: Includes small delays between downloads to be respectful to the API
-
-## Expected Output
-
-```
-🏀 Starting NBA logo download...
-Target directory: assets/sports/nba_logos/
-Force download: False
-✅ NBA logo download complete!
-📊 Summary: 30 downloaded, 0 failed
-🎉 NBA logos are now ready for use in the leaderboard!
-```
-
-## File Structure
-
-After running the script, you'll have:
-```
-assets/sports/nba_logos/
-├── ATL.png # Atlanta Hawks
-├── BOS.png # Boston Celtics
-├── BKN.png # Brooklyn Nets
-├── CHA.png # Charlotte Hornets
-├── CHI.png # Chicago Bulls
-├── CLE.png # Cleveland Cavaliers
-├── DAL.png # Dallas Mavericks
-├── DEN.png # Denver Nuggets
-├── DET.png # Detroit Pistons
-├── GSW.png # Golden State Warriors
-├── HOU.png # Houston Rockets
-├── IND.png # Indiana Pacers
-├── LAC.png # LA Clippers
-├── LAL.png # Los Angeles Lakers
-├── MEM.png # Memphis Grizzlies
-├── MIA.png # Miami Heat
-├── MIL.png # Milwaukee Bucks
-├── MIN.png # Minnesota Timberwolves
-├── NOP.png # New Orleans Pelicans
-├── NYK.png # New York Knicks
-├── OKC.png # Oklahoma City Thunder
-├── ORL.png # Orlando Magic
-├── PHI.png # Philadelphia 76ers
-├── PHX.png # Phoenix Suns
-├── POR.png # Portland Trail Blazers
-├── SAC.png # Sacramento Kings
-├── SAS.png # San Antonio Spurs
-├── TOR.png # Toronto Raptors
-├── UTA.png # Utah Jazz
-└── WAS.png # Washington Wizards
-```
-
-## Integration with NBA plugins
-
-Once the logos are in `assets/sports/nba_logos/`, both the
-`basketball-scoreboard` and `ledmatrix-leaderboard` plugins will pick
-them up automatically and skip their own first-run download. This is
-useful if you want to deploy a Pi without internet access to ESPN, or
-if you want to preview the display on your dev machine without
-waiting for downloads.
-
-## Troubleshooting
-
-### "Import error: No module named 'requests'"
-Make sure you're running this from the LEDMatrix project directory where all dependencies are installed.
-
-### "Permission denied" errors
-Make sure the script has write permissions to the `assets/sports/nba_logos/` directory.
-
-### Some logos fail to download
-This is normal - some teams might have temporary API issues or the ESPN API might be rate-limiting. The script will continue with the successful downloads.
-
-## Requirements
-
-- Python 3.9+ (matches the project's overall minimum)
-- `requests` library (already in `requirements.txt`)
-- Write access to `assets/sports/nba_logos/` directory
diff --git a/scripts/debug/debug_web_manual.py b/scripts/debug/debug_web_manual.py
deleted file mode 100644
index 387a4300..00000000
--- a/scripts/debug/debug_web_manual.py
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/usr/bin/env python3
-"""
-Web Interface Manual Debug Script
-Run this to diagnose why web_interface/start.py isn't working
-"""
-
-import sys
-import os
-import traceback
-from pathlib import Path
-
-def main():
- print("🔍 LED Matrix Web Interface Debug Tool")
- print("=" * 50)
-
- # Change to project root (two levels up from scripts/debug/)
- project_root = Path(__file__).parent.parent.parent.resolve()
- os.chdir(project_root)
- print(f"📁 Working directory: {os.getcwd()}")
-
- # Add to Python path
- sys.path.insert(0, str(project_root))
- print(f"🔗 Python path includes: {project_root}")
-
- print("\n1. Testing basic imports...")
- try:
- import flask
- print(f" ✅ Flask: {flask.__version__}")
- except ImportError as e:
- print(f" ❌ Flask missing: {e}")
- return False
-
- try:
- from src.config_manager import ConfigManager
- print(" ✅ ConfigManager imported")
- except Exception as e:
- print(f" ❌ ConfigManager failed: {e}")
- traceback.print_exc()
- return False
-
- print("\n2. Testing web interface imports...")
- try:
- from web_interface.app import app
- print(" ✅ web_interface.app imported")
- print(f" 📋 App object: {app}")
- except Exception as e:
- print(f" ❌ web_interface.app failed: {e}")
- traceback.print_exc()
- return False
-
- print("\n3. Checking config...")
- try:
- config_manager = ConfigManager()
- config = config_manager.load_config()
- print(" ✅ Config loaded")
-
- # Same rule ledmatrix-web.service applies: only an explicit false/off
- # keeps the web interface down; a missing key means on.
- sys.path.insert(0, str(project_root / 'scripts' / 'utils'))
- from start_web_conditionally import autostart_enabled
- raw = config.get('web_display_autostart', '(not set, defaults to on)')
- state = 'starts' if autostart_enabled(config) else 'will NOT start'
- print(f" 🔧 web_display_autostart: {raw} (web interface {state})")
- except Exception as e:
- print(f" ❌ Config check failed: {e}")
- traceback.print_exc()
- return False
-
- print("\n4. Testing Flask startup...")
- try:
- print(" 🚀 Starting Flask app...")
- print(" 📍 Will run on: http://0.0.0.0:5000")
- print(" ⏹️ Press Ctrl+C to stop")
-
- # Run the app (debug mode controlled by env var to satisfy security scanners)
- _debug = os.environ.get('LEDMATRIX_FLASK_DEBUG', '0') == '1'
- app.run(host='0.0.0.0', port=5000, debug=_debug)
-
- except KeyboardInterrupt:
- print("\n ⏹️ Server stopped by user")
- return True
- except Exception as e:
- print(f" ❌ Flask startup failed: {e}")
- traceback.print_exc()
- return False
-
-if __name__ == "__main__":
- try:
- success = main()
- if success:
- print("\n✅ Debug completed successfully")
- else:
- print("\n❌ Debug found issues - check output above")
- except Exception as e:
- print(f"\n💥 Debug script crashed: {e}")
- traceback.print_exc()
diff --git a/scripts/dev/README.md b/scripts/dev/README.md
index cec5a631..01b1ebc9 100644
--- a/scripts/dev/README.md
+++ b/scripts/dev/README.md
@@ -6,7 +6,6 @@ This directory contains scripts and utilities for development and testing.
- **`dev_plugin_setup.sh`** - Sets up plugin development environment by linking plugin repositories
- **`run_emulator.sh`** - Runs the LED Matrix display in emulator mode (for development without hardware)
-- **`validate_python.py`** - Validates Python files for common formatting and syntax errors
## Usage
@@ -30,8 +29,3 @@ links. To use a fork or another clone location, copy
./scripts/dev/run_emulator.sh
```
-### Validating Python Files
-```bash
-python3 scripts/dev/validate_python.py
-```
-
diff --git a/scripts/dev/validate_python.py b/scripts/dev/validate_python.py
deleted file mode 100644
index b10a3ee9..00000000
--- a/scripts/dev/validate_python.py
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env python3
-"""
-Python file validation script to prevent common formatting errors.
-
-This script checks for:
-1. Proper indentation (4 spaces, no mixed tabs/spaces)
-2. Missing imports
-3. Syntax errors
-4. Line length issues
-5. Proper try/except structure
-
-Usage: python tools/validate_python.py
-"""
-
-import ast
-import sys
-import os
-
-def validate_file(filepath: str) -> bool:
- """Validate a Python file for common issues."""
- try:
- with open(filepath, 'r', encoding='utf-8') as f:
- content = f.read()
-
- issues_found = []
-
- # Check for tabs (should use spaces)
- if '\t' in content:
- issues_found.append("❌ Contains tabs - use 4 spaces instead")
-
- # Check for trailing whitespace
- lines = content.split('\n')
- for i, line in enumerate(lines, 1):
- if line.rstrip() != line:
- issues_found.append(f"❌ Line {i}: Trailing whitespace")
-
- # Check for very long lines
- for i, line in enumerate(lines, 1):
- if len(line) > 120:
- issues_found.append(f"⚠️ Line {i}: Very long line ({len(line)} chars)")
-
- # Check for proper try/except structure
- try:
- ast.parse(content)
- except SyntaxError as e:
- issues_found.append(f"❌ Syntax error: {e}")
-
- # Check for mixed quotes (inconsistency)
- single_quotes = content.count("'")
- double_quotes = content.count('"')
- if single_quotes > 0 and double_quotes > 0:
- issues_found.append("⚠️ Mixed quote usage - consider using double quotes consistently")
-
- # Report results
- if issues_found:
- print(f"\n🔍 Validation Results for: {filepath}")
- print("=" * 50)
- for issue in issues_found:
- print(issue)
- return False
- else:
- print(f"✅ {filepath} - All checks passed!")
- return True
-
- except Exception as e:
- print(f"❌ Error reading file {filepath}: {e}")
- return False
-
-def validate_directory(directory: str) -> bool:
- """Validate all Python files in a directory."""
- all_passed = True
-
- for root, dirs, files in os.walk(directory):
- for file in files:
- if file.endswith('.py'):
- filepath = os.path.join(root, file)
- if not validate_file(filepath):
- all_passed = False
-
- return all_passed
-
-if __name__ == "__main__":
- if len(sys.argv) != 2:
- print("Usage: python tools/validate_python.py ")
- sys.exit(1)
-
- target = sys.argv[1]
-
- if os.path.isfile(target):
- validate_file(target)
- elif os.path.isdir(target):
- validate_directory(target)
- else:
- print(f"❌ Path not found: {target}")
- sys.exit(1)
diff --git a/scripts/diagnose_plugin_permissions.sh b/scripts/diagnose_plugin_permissions.sh
deleted file mode 100755
index 3e3b3104..00000000
--- a/scripts/diagnose_plugin_permissions.sh
+++ /dev/null
@@ -1,171 +0,0 @@
-#!/bin/bash
-# Diagnostic script for plugin directory permissions
-# Run this on the Raspberry Pi to check and fix plugin directory permissions
-
-set -e
-
-echo "=========================================="
-echo "Plugin Directory Permissions Diagnostic"
-echo "=========================================="
-echo ""
-
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-NC='\033[0m' # No Color
-
-# Get the actual user
-if [ -n "$SUDO_USER" ]; then
- ACTUAL_USER="$SUDO_USER"
-else
- ACTUAL_USER=$(whoami)
-fi
-
-# Get project root (parent of scripts/ directory)
-SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-PROJECT_ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-PLUGINS_DIR="$PROJECT_ROOT_DIR/plugins"
-PLUGIN_REPOS_DIR="$PROJECT_ROOT_DIR/plugin-repos"
-
-echo "Project root: $PROJECT_ROOT_DIR"
-echo "User: $ACTUAL_USER"
-echo "Running as: $(whoami)"
-echo ""
-
-# Check parent directory permissions
-echo "=== Parent Directory Permissions ==="
-echo "Checking /home/$ACTUAL_USER:"
-if [ -d "/home/$ACTUAL_USER" ]; then
- PERMS=$(stat -c "%a %U:%G" "/home/$ACTUAL_USER")
- echo " Permissions: $PERMS"
- if [ "$(stat -c "%a" "/home/$ACTUAL_USER")" != "755" ] && [ "$(stat -c "%a" "/home/$ACTUAL_USER")" != "750" ]; then
- echo -e " ${YELLOW}⚠ Warning: Home directory has restrictive permissions${NC}"
- echo " Root may not be able to access subdirectories"
- fi
-else
- echo -e " ${RED}✗ Home directory not found${NC}"
-fi
-echo ""
-
-echo "Checking $PROJECT_ROOT_DIR:"
-if [ -d "$PROJECT_ROOT_DIR" ]; then
- PERMS=$(stat -c "%a %U:%G" "$PROJECT_ROOT_DIR")
- echo " Permissions: $PERMS"
- if [ "$(stat -c "%a" "$PROJECT_ROOT_DIR")" != "755" ] && [ "$(stat -c "%a" "$PROJECT_ROOT_DIR")" != "750" ]; then
- echo -e " ${YELLOW}⚠ Warning: Project directory has restrictive permissions${NC}"
- fi
-else
- echo -e " ${RED}✗ Project directory not found${NC}"
-fi
-echo ""
-
-# Check plugins directory
-echo "=== Plugins Directory ==="
-if [ -d "$PLUGINS_DIR" ]; then
- PERMS=$(stat -c "%a %U:%G" "$PLUGINS_DIR")
- echo " Path: $PLUGINS_DIR"
- echo " Permissions: $PERMS"
-
- OWNER=$(stat -c "%U" "$PLUGINS_DIR")
- GROUP=$(stat -c "%G" "$PLUGINS_DIR")
- PERM_BITS=$(stat -c "%a" "$PLUGINS_DIR")
-
- if [ "$OWNER" = "root" ] && [ "$GROUP" = "$ACTUAL_USER" ] && [ "$PERM_BITS" = "775" ]; then
- echo -e " ${GREEN}✓ Correct permissions${NC}"
- else
- echo -e " ${RED}✗ Incorrect permissions${NC}"
- echo " Expected: root:$ACTUAL_USER 775"
- echo " Actual: $OWNER:$GROUP $PERM_BITS"
- fi
-
- # Check if root can access
- if sudo -u root test -r "$PLUGINS_DIR" && sudo -u root test -w "$PLUGINS_DIR"; then
- echo -e " ${GREEN}✓ Root can read/write${NC}"
- else
- echo -e " ${RED}✗ Root cannot access${NC}"
- fi
-else
- echo -e " ${YELLOW}⚠ Directory does not exist${NC}"
-fi
-echo ""
-
-# Check plugin-repos directory
-echo "=== Plugin-Repos Directory ==="
-if [ -d "$PLUGIN_REPOS_DIR" ]; then
- PERMS=$(stat -c "%a %U:%G" "$PLUGIN_REPOS_DIR")
- echo " Path: $PLUGIN_REPOS_DIR"
- echo " Permissions: $PERMS"
-
- OWNER=$(stat -c "%U" "$PLUGIN_REPOS_DIR")
- GROUP=$(stat -c "%G" "$PLUGIN_REPOS_DIR")
- PERM_BITS=$(stat -c "%a" "$PLUGIN_REPOS_DIR")
-
- if [ "$OWNER" = "root" ] && [ "$GROUP" = "$ACTUAL_USER" ] && [ "$PERM_BITS" = "775" ]; then
- echo -e " ${GREEN}✓ Correct permissions${NC}"
- else
- echo -e " ${RED}✗ Incorrect permissions${NC}"
- echo " Expected: root:$ACTUAL_USER 775"
- echo " Actual: $OWNER:$GROUP $PERM_BITS"
- fi
-
- # Check if root can access
- if sudo -u root test -r "$PLUGIN_REPOS_DIR" && sudo -u root test -w "$PLUGIN_REPOS_DIR"; then
- echo -e " ${GREEN}✓ Root can read/write${NC}"
- else
- echo -e " ${RED}✗ Root cannot access${NC}"
- fi
-
- # Try to list contents as root
- echo " Testing root access:"
- if sudo -u root ls "$PLUGIN_REPOS_DIR" >/dev/null 2>&1; then
- echo -e " ${GREEN}✓ Root can list directory${NC}"
- else
- echo -e " ${RED}✗ Root cannot list directory${NC}"
- echo " Error: $(sudo -u root ls "$PLUGIN_REPOS_DIR" 2>&1 | head -1)"
- fi
-else
- echo -e " ${YELLOW}⚠ Directory does not exist${NC}"
-fi
-echo ""
-
-# Test if root can create files
-echo "=== Testing Root Write Access ==="
-TEST_FILE="$PLUGIN_REPOS_DIR/.permission_test_$$"
-if sudo -u root touch "$TEST_FILE" 2>/dev/null; then
- echo -e " ${GREEN}✓ Root can create files${NC}"
- sudo -u root rm -f "$TEST_FILE"
-else
- echo -e " ${RED}✗ Root cannot create files${NC}"
- echo " Error: $(sudo -u root touch "$TEST_FILE" 2>&1)"
-fi
-echo ""
-
-# Summary and fix recommendations
-echo "=== Summary ==="
-NEEDS_FIX=false
-
-if [ ! -d "$PLUGIN_REPOS_DIR" ]; then
- echo -e "${YELLOW}⚠ plugin-repos directory does not exist${NC}"
- NEEDS_FIX=true
-elif [ "$(stat -c "%U:%G" "$PLUGIN_REPOS_DIR" 2>/dev/null)" != "root:$ACTUAL_USER" ] || [ "$(stat -c "%a" "$PLUGIN_REPOS_DIR" 2>/dev/null)" != "775" ]; then
- echo -e "${RED}✗ plugin-repos has incorrect permissions${NC}"
- NEEDS_FIX=true
-fi
-
-if [ "$NEEDS_FIX" = true ]; then
- echo ""
- echo "=== Fix Commands ==="
- echo "Run these commands to fix permissions:"
- echo ""
- echo "sudo mkdir -p $PLUGIN_REPOS_DIR"
- echo "sudo chown root:$ACTUAL_USER $PLUGIN_REPOS_DIR"
- echo "sudo chmod 775 $PLUGIN_REPOS_DIR"
- echo ""
- echo "Or run the fix script:"
- echo "sudo bash $PROJECT_ROOT_DIR/scripts/fix_perms/fix_plugin_permissions.sh"
-else
- echo -e "${GREEN}✓ All permissions look correct${NC}"
-fi
-echo ""
-
diff --git a/scripts/diagnose_web_ui.sh b/scripts/diagnose_web_ui.sh
deleted file mode 100755
index 1bc26306..00000000
--- a/scripts/diagnose_web_ui.sh
+++ /dev/null
@@ -1,232 +0,0 @@
-#!/bin/bash
-# LEDMatrix Web UI Diagnostic Script
-# Run this on your Raspberry Pi to diagnose web UI startup issues
-
-set -e
-
-echo "=========================================="
-echo "LEDMatrix Web UI Diagnostic Report"
-echo "=========================================="
-echo ""
-
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-NC='\033[0m' # No Color
-
-# Check if running as root or with sudo
-if [ "$EUID" -ne 0 ]; then
- echo -e "${YELLOW}Warning: Some checks require sudo. Running what we can...${NC}"
-fi
-
-PROJECT_DIR="${HOME}/LEDMatrix"
-
-# Report web_display_autostart the way scripts/utils/start_web_conditionally.py
-# (what ledmatrix-web.service runs) decides it: only an explicit false/off keeps
-# the web interface down; a missing key or an unreadable config starts it.
-# Prints "on ", "off ", "default" (key not set) or "unreadable".
-web_autostart_state() {
- (cd "$1" && python3 - 2>/dev/null <<'PY'
-import json, os, sys
-sys.path.insert(0, os.path.join(os.getcwd(), "scripts", "utils"))
-try:
- from start_web_conditionally import autostart_enabled
-except Exception:
- def autostart_enabled(config):
- value = config.get("web_display_autostart", True)
- if isinstance(value, str):
- return value.strip().lower() not in ("off", "false", "no", "0")
- return bool(value)
-try:
- with open(os.path.join("config", "config.json"), encoding="utf-8") as f:
- config = json.load(f)
-except Exception:
- config = None
-if not isinstance(config, dict):
- print("unreadable")
-elif "web_display_autostart" not in config:
- print("default")
-else:
- raw = json.dumps(config["web_display_autostart"])
- print(("on " if autostart_enabled(config) else "off ") + raw)
-PY
- ) || echo "unknown"
-}
-
-echo "1. Checking service status..."
-echo "------------------------------"
-if systemctl is-active --quiet ledmatrix-web 2>/dev/null || sudo systemctl is-active --quiet ledmatrix-web 2>/dev/null; then
- echo -e "${GREEN}✓ Service is ACTIVE${NC}"
- STATUS=$(sudo systemctl status ledmatrix-web --no-pager -l | head -n 3)
- echo "$STATUS"
-else
- echo -e "${RED}✗ Service is NOT running${NC}"
- sudo systemctl status ledmatrix-web --no-pager -l | head -n 10 || echo "Service may not be installed"
-fi
-echo ""
-
-echo "2. Checking if service is enabled..."
-echo "------------------------------"
-if systemctl is-enabled --quiet ledmatrix-web 2>/dev/null || sudo systemctl is-enabled --quiet ledmatrix-web 2>/dev/null; then
- echo -e "${GREEN}✓ Service is enabled to start on boot${NC}"
-else
- echo -e "${YELLOW}⚠ Service is NOT enabled (won't start on boot)${NC}"
-fi
-echo ""
-
-echo "3. Checking configuration file..."
-echo "------------------------------"
-if [ -f "${PROJECT_DIR}/config/config.json" ]; then
- echo -e "${GREEN}✓ Config file exists${NC}"
- AUTOSTART=$(web_autostart_state "$PROJECT_DIR")
- case "$AUTOSTART" in
- on\ *)
- echo -e "${GREEN}✓ web_display_autostart is ${AUTOSTART#on } (web UI starts)${NC}"
- ;;
- off\ *)
- echo -e "${RED}✗ web_display_autostart is ${AUTOSTART#off } (web UI won't start!)${NC}"
- echo " Fix: Edit config.json and set 'web_display_autostart': true"
- ;;
- default)
- echo -e "${GREEN}✓ web_display_autostart is not set (defaults to on; web UI starts)${NC}"
- ;;
- unreadable)
- echo -e "${YELLOW}⚠ config.json could not be parsed (the web UI still starts so it can be repaired)${NC}"
- ;;
- *)
- echo -e "${YELLOW}⚠ Could not evaluate web_display_autostart (python3 unavailable?)${NC}"
- ;;
- esac
-else
- echo -e "${RED}✗ Config file NOT FOUND at ${PROJECT_DIR}/config/config.json${NC}"
-fi
-echo ""
-
-echo "4. Checking recent service logs..."
-echo "------------------------------"
-RECENT_LOGS=$(sudo journalctl -u ledmatrix-web -n 30 --no-pager 2>/dev/null || echo "No logs available")
-if [ -n "$RECENT_LOGS" ] && [ "$RECENT_LOGS" != "No logs available" ]; then
- echo "$RECENT_LOGS"
- echo ""
- echo "Key messages from logs:"
- echo "$RECENT_LOGS" | grep -i "web_display_autostart\|Configuration\|Launching\|will not\|Failed\|Error\|Starting" || echo " (no key messages found)"
-else
- echo -e "${YELLOW}⚠ No recent logs found${NC}"
-fi
-echo ""
-
-echo "5. Checking web interface files..."
-echo "------------------------------"
-FILES_TO_CHECK=(
- "scripts/utils/start_web_conditionally.py"
- "web_interface/start.py"
- "web_interface/app.py"
- "web_interface/requirements.txt"
- "web_interface/blueprints/api_v3/__init__.py"
- "web_interface/blueprints/pages_v3.py"
-)
-
-for file in "${FILES_TO_CHECK[@]}"; do
- if [ -f "${PROJECT_DIR}/${file}" ]; then
- echo -e "${GREEN}✓ ${file}${NC}"
- else
- echo -e "${RED}✗ ${file} MISSING${NC}"
- fi
-done
-echo ""
-
-echo "6. Checking Python import test..."
-echo "------------------------------"
-cd "${PROJECT_DIR}" 2>/dev/null || {
- echo -e "${RED}✗ Cannot access project directory: ${PROJECT_DIR}${NC}"
- echo ""
- exit 1
-}
-
-if python3 -c "from web_interface.app import app; print('OK')" 2>&1; then
- echo -e "${GREEN}✓ Flask app imports successfully${NC}"
-else
- echo -e "${RED}✗ Flask app import FAILED${NC}"
- echo " Error details:"
- python3 -c "from web_interface.app import app; print('OK')" 2>&1 | head -n 10
-fi
-echo ""
-
-echo "7. Checking port 5000 availability..."
-echo "------------------------------"
-if command -v lsof &> /dev/null; then
- PORT_CHECK=$(sudo lsof -i :5000 2>/dev/null || echo "")
- if [ -z "$PORT_CHECK" ]; then
- echo -e "${GREEN}✓ Port 5000 is available${NC}"
- else
- echo -e "${YELLOW}⚠ Port 5000 is in use:${NC}"
- echo "$PORT_CHECK"
- fi
-elif command -v ss &> /dev/null; then
- PORT_CHECK=$(sudo ss -tlnp | grep :5000 || echo "")
- if [ -z "$PORT_CHECK" ]; then
- echo -e "${GREEN}✓ Port 5000 is available${NC}"
- else
- echo -e "${YELLOW}⚠ Port 5000 is in use:${NC}"
- echo "$PORT_CHECK"
- fi
-else
- echo -e "${YELLOW}⚠ Cannot check port (lsof/ss not available)${NC}"
-fi
-echo ""
-
-echo "8. Checking service file..."
-echo "------------------------------"
-if [ -f "/etc/systemd/system/ledmatrix-web.service" ]; then
- echo -e "${GREEN}✓ Service file exists${NC}"
- echo " Location: /etc/systemd/system/ledmatrix-web.service"
- echo " WorkingDirectory: $(grep WorkingDirectory /etc/systemd/system/ledmatrix-web.service | cut -d'=' -f2 || echo 'not found')"
- echo " ExecStart: $(grep ExecStart /etc/systemd/system/ledmatrix-web.service | cut -d'=' -f2 || echo 'not found')"
-else
- echo -e "${RED}✗ Service file NOT FOUND${NC}"
- echo " Run: sudo ./install_web_service.sh (if available)"
-fi
-echo ""
-
-echo "9. Checking dependencies..."
-echo "------------------------------"
-if [ -f "${PROJECT_DIR}/web_interface/requirements.txt" ]; then
- echo "Checking if Flask is installed..."
- if python3 -c "import flask; print(f'Flask {flask.__version__}')" 2>/dev/null; then
- echo -e "${GREEN}✓ Flask is installed${NC}"
- else
- echo -e "${RED}✗ Flask is NOT installed${NC}"
- echo " Run: pip3 install --break-system-packages -r web_interface/requirements.txt"
- fi
-else
- echo -e "${YELLOW}⚠ requirements.txt not found${NC}"
-fi
-echo ""
-
-echo "10. Manual startup test (dry run)..."
-echo "------------------------------"
-echo "To test manual startup, run:"
-echo " cd ${PROJECT_DIR}"
-echo " python3 web_interface/start.py"
-echo ""
-
-echo "=========================================="
-echo "Diagnostic Summary"
-echo "=========================================="
-echo ""
-echo "Most common issues:"
-echo " 1. web_display_autostart is set to false in config.json (a missing key means on)"
-echo " 2. Service not enabled or not started"
-echo " 3. Missing dependencies (Flask, etc.)"
-echo " 4. Import errors in web_interface/app.py"
-echo " 5. Port 5000 already in use"
-echo ""
-echo "Next steps:"
-echo " - Review the checks above for any RED ✗ marks"
-echo " - Check recent logs: sudo journalctl -u ledmatrix-web -n 50 --no-pager"
-echo " - Follow logs in real-time: sudo journalctl -u ledmatrix-web -f"
-echo " - Try manual start: cd ${PROJECT_DIR} && python3 web_interface/start.py"
-echo ""
-echo "=========================================="
-
diff --git a/scripts/download_nba_logos.py b/scripts/download_nba_logos.py
deleted file mode 100644
index 99bb5b4b..00000000
--- a/scripts/download_nba_logos.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/usr/bin/env python3
-"""
-Script to download all NBA team logos from ESPN API and save them in assets/sports/nba_logos/
-"""
-import sys
-import os
-import logging
-from typing import Tuple
-
-# Add the project root to Python path so we can import the logo downloader
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-# Set up logging
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def download_nba_logos(force_download: bool = False) -> Tuple[int, int]:
- """
- Download all NBA team logos from ESPN API.
-
- Args:
- force_download: Whether to re-download existing logos
-
- Returns:
- Tuple of (downloaded_count, failed_count)
- """
- try:
- from src.logo_downloader import download_all_logos_for_league
-
- logger.info("🏀 Starting NBA logo download...")
- logger.info(f"Target directory: assets/sports/nba_logos/")
- logger.info(f"Force download: {force_download}")
-
- # Use the existing function to download all NBA logos
- downloaded_count, failed_count = download_all_logos_for_league('nba', force_download)
-
- logger.info("✅ NBA logo download complete!")
- logger.info(f"📊 Summary: {downloaded_count} downloaded, {failed_count} failed")
-
- if downloaded_count > 0:
- logger.info("🎉 NBA logos are now ready for use in the leaderboard!")
- else:
- logger.info("ℹ️ All NBA logos were already present or download failed for all teams")
-
- return downloaded_count, failed_count
-
- except ImportError as e:
- logger.error(f"❌ Import error: {e}")
- logger.info("💡 Make sure you're running this from the LEDMatrix project directory")
- return 0, 0
- except Exception as e:
- logger.error(f"❌ Unexpected error: {e}")
- return 0, 0
-
-def main():
- """Main function with command line argument parsing."""
- import argparse
-
- parser = argparse.ArgumentParser(description='Download all NBA team logos from ESPN API')
- parser.add_argument(
- '--force',
- action='store_true',
- help='Force re-download of existing logos'
- )
- parser.add_argument(
- '--quiet',
- action='store_true',
- help='Reduce logging output'
- )
-
- args = parser.parse_args()
-
- # Set logging level based on quiet flag
- if args.quiet:
- logging.getLogger().setLevel(logging.WARNING)
-
- logger.info("🚀 NBA Logo Downloader")
- logger.info("=" * 50)
-
- # Download the logos
- downloaded, failed = download_nba_logos(args.force)
-
- # Exit with appropriate code
- if failed > 0 and downloaded == 0:
- logger.error("❌ All downloads failed!")
- sys.exit(1)
- elif failed > 0:
- logger.warning(f"⚠️ {failed} downloads failed, but {downloaded} succeeded")
- sys.exit(0) # Partial success is still success
- else:
- logger.info("🎉 All NBA logos downloaded successfully!")
- sys.exit(0)
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/fix_internet_connectivity.sh b/scripts/fix_internet_connectivity.sh
deleted file mode 100755
index 6971c6ba..00000000
--- a/scripts/fix_internet_connectivity.sh
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/bin/bash
-# Emergency script to fix internet connectivity issues
-# Run this if the Pi can't access the internet after AP mode testing
-
-echo "=========================================="
-echo "Internet Connectivity Fix Script"
-echo "=========================================="
-echo ""
-
-# 1. Disable IP forwarding
-echo "1. Disabling IP forwarding..."
-sudo sysctl -w net.ipv4.ip_forward=0
-echo " ✓ IP forwarding disabled"
-
-# 2. Stop and disable dnsmasq (if it's interfering)
-echo ""
-echo "2. Checking dnsmasq..."
-if sudo systemctl is-active dnsmasq > /dev/null 2>&1; then
- echo " ⚠ dnsmasq is running - stopping it..."
- sudo systemctl stop dnsmasq
- sudo systemctl disable dnsmasq
- echo " ✓ dnsmasq stopped"
-else
- echo " ✓ dnsmasq is not running"
-fi
-
-# 3. Restore dnsmasq config if backup exists
-echo ""
-echo "3. Checking dnsmasq config..."
-if [ -f /etc/dnsmasq.conf.backup ]; then
- echo " ⚠ Found backup config - restoring..."
- sudo cp /etc/dnsmasq.conf.backup /etc/dnsmasq.conf
- echo " ✓ Config restored"
-else
- echo " ✓ No backup config found (normal)"
-fi
-
-# 4. Restart NetworkManager to restore DNS
-echo ""
-echo "4. Restarting NetworkManager..."
-sudo systemctl restart NetworkManager
-sleep 2
-echo " ✓ NetworkManager restarted"
-
-# 5. Check DNS resolution
-echo ""
-echo "5. Testing DNS resolution..."
-if nslookup google.com > /dev/null 2>&1; then
- echo " ✓ DNS resolution working"
-else
- echo " ✗ DNS resolution failed"
- echo " → Try: sudo systemctl restart systemd-resolved"
-fi
-
-# 6. Test internet connectivity
-echo ""
-echo "6. Testing internet connectivity..."
-if ping -c 2 8.8.8.8 > /dev/null 2>&1; then
- echo " ✓ Internet connectivity working"
-else
- echo " ✗ Internet connectivity failed"
- echo " → Check: ip route show"
- echo " → Check: ip addr show"
-fi
-
-# 7. Check if AP mode is still active
-echo ""
-echo "7. Checking AP mode status..."
-if sudo systemctl is-active hostapd > /dev/null 2>&1; then
- echo " ⚠ AP mode is still active!"
- echo " → To disable: cd ~/LEDMatrix && python3 -c 'from src.wifi_manager import WiFiManager; wm = WiFiManager(); wm.disable_ap_mode()'"
-else
- echo " ✓ AP mode is not active"
-fi
-
-# 8. Remove any leftover iptables rules
-echo ""
-echo "8. Checking iptables rules..."
-if command -v iptables > /dev/null 2>&1; then
- # Try to remove any port 80 redirect rules
- sudo iptables -t nat -D PREROUTING -i wlan0 -p tcp --dport 80 -j REDIRECT --to-port 5000 2>/dev/null
- sudo iptables -D INPUT -i wlan0 -p tcp --dport 80 -j ACCEPT 2>/dev/null
- echo " ✓ Cleaned up iptables rules (if any existed)"
-else
- echo " → iptables not available (normal on some systems)"
-fi
-
-echo ""
-echo "=========================================="
-echo "Fix Complete"
-echo "=========================================="
-echo ""
-echo "If internet still doesn't work:"
-echo "1. Check: ip route show"
-echo "2. Check: cat /etc/resolv.conf"
-echo "3. Restart network: sudo systemctl restart NetworkManager"
-echo "4. Check Ethernet connection: ip link show eth0"
-echo ""
-
-
-
-
-
-
-
-
-
-
-
diff --git a/scripts/install/README.md b/scripts/install/README.md
index dc092d9d..544ea336 100644
--- a/scripts/install/README.md
+++ b/scripts/install/README.md
@@ -19,9 +19,6 @@ This directory contains scripts for installing and configuring the LEDMatrix sys
(the user who runs the script, i.e. the one you installed LEDMatrix as;
there is no `ledmatrix` system user) the passwordless `nmcli` and related
WiFi permissions the web interface needs
-- **`migrate_config.sh`** - Migrates configuration files to new formats (if needed)
-- **`debug_install.sh`** - Diagnostic helper used when an install
- fails; collects environment info and recent logs
## Usage
diff --git a/scripts/install/debug_install.sh b/scripts/install/debug_install.sh
deleted file mode 100755
index 7f094217..00000000
--- a/scripts/install/debug_install.sh
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/bin/bash
-# Quick diagnostic script to check why first_time_install.sh is failing
-# Run this on the Pi: bash debug_install.sh
-
-echo "=== Diagnostic Script for Installation Failure ==="
-echo ""
-
-echo "1. Checking if running as root:"
-if [ "$EUID" -eq 0 ]; then
- echo " ✓ Running as root (EUID=$EUID)"
-else
- echo " ✗ NOT running as root (EUID=$EUID, user=$(whoami))"
-fi
-echo ""
-
-echo "2. Checking if first_time_install.sh exists:"
-if [ -f "./first_time_install.sh" ]; then
- echo " ✓ Found ./first_time_install.sh"
- echo " Checking if executable:"
- if [ -x "./first_time_install.sh" ]; then
- echo " ✓ Is executable"
- else
- echo " ✗ NOT executable (fix with: chmod +x first_time_install.sh)"
- fi
-else
- echo " ✗ NOT found in current directory"
- echo " Current directory: $(pwd)"
-fi
-echo ""
-
-echo "3. Testing argument passing with -y flag:"
-echo " Running: bash ./first_time_install.sh -y --help 2>&1 | head -20"
-if [ -f "./first_time_install.sh" ]; then
- bash ./first_time_install.sh -y --help 2>&1 | head -20 || echo " ✗ Script failed or not found"
-else
- echo " ✗ first_time_install.sh not found"
-fi
-echo ""
-
-echo "4. Checking environment variable:"
-echo " LEDMATRIX_ASSUME_YES=${LEDMATRIX_ASSUME_YES:-not set}"
-echo " Testing with env: env LEDMATRIX_ASSUME_YES=1 bash -c 'echo ASSUME_YES would be set'"
-env LEDMATRIX_ASSUME_YES=1 bash -c 'echo " ASSUME_YES would be: ${LEDMATRIX_ASSUME_YES:-not set}"'
-echo ""
-
-echo "5. Testing sudo with arguments:"
-echo " Command: sudo -E env LEDMATRIX_ASSUME_YES=1 bash ./first_time_install.sh -y --help 2>&1 | head -20"
-if [ -f "./first_time_install.sh" ]; then
- sudo -E env LEDMATRIX_ASSUME_YES=1 bash ./first_time_install.sh -y --help 2>&1 | head -20 || echo " ✗ Sudo command failed"
-else
- echo " ✗ first_time_install.sh not found"
-fi
-echo ""
-
-echo "6. Checking /tmp permissions:"
-echo " /tmp is writable: $([ -w /tmp ] && echo 'YES' || echo 'NO')"
-echo " /tmp permissions: $(stat -c '%a' /tmp 2>/dev/null || echo 'unknown')"
-echo " TMPDIR: ${TMPDIR:-not set}"
-echo ""
-
-echo "7. Checking stdin/TTY:"
-if [ -t 0 ]; then
- echo " ✓ stdin is a TTY (interactive)"
-else
- echo " ✗ stdin is NOT a TTY (non-interactive/pipe)"
- echo " This is expected when running via curl | bash"
-fi
-echo ""
-
-echo "8. Latest installation log:"
-# Determine project root directory (parent of scripts/install/)
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
-LOG_DIR="$PROJECT_ROOT_DIR/logs"
-LOG_FILE=$(ls -t "$LOG_DIR"/first_time_install_*.log 2>/dev/null | head -1)
-if [ -n "$LOG_FILE" ]; then
- echo " Found: $LOG_FILE"
- echo " Last 30 lines:"
- tail -30 "$LOG_FILE" | sed 's/^/ /'
-else
- echo " No log files found in $LOG_DIR/"
-fi
-echo ""
-
-echo "=== Diagnostic Complete ==="
diff --git a/scripts/install/migrate_config.sh b/scripts/install/migrate_config.sh
deleted file mode 100755
index 0000bec4..00000000
--- a/scripts/install/migrate_config.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/bash
-
-# LED Matrix Configuration Migration Script
-# This script helps migrate existing config.json to the new template-based system
-
-set -e
-
-echo "=========================================="
-echo "LED Matrix Configuration Migration Script"
-echo "=========================================="
-echo ""
-
-# Check if we're in the right directory
-if [ ! -f "config/config.template.json" ]; then
- echo "Error: config/config.template.json not found."
- echo "Please run this script from the LEDMatrix project root directory."
- exit 1
-fi
-
-# Check if config.json exists
-if [ ! -f "config/config.json" ]; then
- echo "No existing config.json found. Creating from template..."
- cp config/config.template.json config/config.json
- echo "✓ Created config/config.json from template"
- echo ""
- echo "You can now edit config/config.json with your preferences."
- exit 0
-fi
-
-echo "Existing config.json found. The system will automatically handle migration."
-echo ""
-echo "What this means:"
-echo "- Your current config.json will be preserved"
-echo "- New configuration options will be automatically added with default values"
-echo "- A backup will be created before any changes"
-echo "- The system handles this automatically when it starts"
-echo ""
-echo "No manual migration is needed. The ConfigManager will handle everything automatically."
-echo ""
-echo "To see the latest configuration options, you can reference:"
-echo " config/config.template.json"
-echo ""
-echo "Migration complete!"
diff --git a/scripts/setup_plugin_repos.py b/scripts/setup_plugin_repos.py
deleted file mode 100755
index 060637b9..00000000
--- a/scripts/setup_plugin_repos.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env python3
-"""
-Setup plugin repository symlinks for local development.
-
-Creates symlinks in plugin-repos/ pointing to plugin directories
-in the ledmatrix-plugins monorepo.
-"""
-
-import json
-import os
-import re
-import sys
-from pathlib import Path
-
-PROJECT_ROOT = Path(__file__).parent.parent
-PLUGIN_REPOS_DIR = PROJECT_ROOT / "plugin-repos"
-MONOREPO_PLUGINS = PROJECT_ROOT.parent / "ledmatrix-plugins" / "plugins"
-
-
-def parse_json_with_trailing_commas(text: str) -> dict:
- """Parse JSON that may have trailing commas.
-
- Note: The regex also matches commas inside string values (e.g., "hello, }").
- This is fine for manifest files but may corrupt complex JSON with such patterns.
- """
- text = re.sub(r",\s*([}\]])", r"\1", text)
- return json.loads(text)
-
-
-def create_symlinks() -> bool:
- """Create symlinks in plugin-repos/ pointing to monorepo plugin dirs."""
- if not MONOREPO_PLUGINS.exists():
- print(f"Error: Monorepo plugins directory not found: {MONOREPO_PLUGINS}")
- return False
-
- PLUGIN_REPOS_DIR.mkdir(exist_ok=True)
-
- created = 0
- skipped = 0
-
- print("Setting up plugin symlinks...")
- print(f" Source: {MONOREPO_PLUGINS}")
- print(f" Links: {PLUGIN_REPOS_DIR}")
- print()
-
- for plugin_dir in sorted(MONOREPO_PLUGINS.iterdir()):
- if not plugin_dir.is_dir():
- continue
- manifest_path = plugin_dir / "manifest.json"
- if not manifest_path.exists():
- continue
-
- try:
- with open(manifest_path, "r", encoding="utf-8") as f:
- manifest = parse_json_with_trailing_commas(f.read())
- except (OSError, json.JSONDecodeError) as e:
- print(f" {plugin_dir.name} - failed to read {manifest_path}: {e}")
- continue
- plugin_id = manifest.get("id", plugin_dir.name)
- link_path = PLUGIN_REPOS_DIR / plugin_id
-
- if link_path.exists() or link_path.is_symlink():
- if link_path.is_symlink():
- try:
- if link_path.resolve() == plugin_dir.resolve():
- skipped += 1
- continue
- else:
- link_path.unlink()
- except OSError:
- link_path.unlink()
- else:
- print(f" {plugin_id} - exists but is not a symlink, skipping")
- skipped += 1
- continue
-
- relative_path = os.path.relpath(plugin_dir, link_path.parent)
- link_path.symlink_to(relative_path)
- print(f" {plugin_id} - linked")
- created += 1
-
- print(f"\nCreated {created} links, skipped {skipped}")
- return True
-
-
-def main():
- print("Setting up plugin repository symlinks from monorepo...\n")
- if not create_symlinks():
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/utils/README.md b/scripts/utils/README.md
index 61bd2ac4..9339d65d 100644
--- a/scripts/utils/README.md
+++ b/scripts/utils/README.md
@@ -7,8 +7,6 @@ This directory contains utility scripts for maintenance and system operations.
- **`clear_cache.py`** - Clears LEDMatrix cache data (specific keys or all cache)
- **`start_web_conditionally.py`** - Conditionally starts the web interface based on config settings
- **`wifi_monitor_daemon.py`** - Background daemon that monitors WiFi/Ethernet connection and manages access point mode
-- **`cleanup_venv.sh`** - Cleans up Python virtual environment files
-- **`clear_python_cache.sh`** - Clears Python cache files (__pycache__, *.pyc, etc.)
- **`pixlet_config_editor.sh`** - Opens Pixlet's own config UI for one installed Starlark app
- **`apply_dns_single_request.sh`** - Adds `options single-request` to the resolver (run by `ledmatrix-dns-fix.service`)
diff --git a/scripts/utils/cleanup_venv.sh b/scripts/utils/cleanup_venv.sh
deleted file mode 100755
index 887289c5..00000000
--- a/scripts/utils/cleanup_venv.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/bash
-
-# Cleanup script to remove virtual environment if it exists
-# This script removes the venv_web_v2 directory if it exists
-
-set -e
-
-# Get the directory where this script is located
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-cd "$SCRIPT_DIR"
-
-echo "Cleaning up virtual environment..."
-
-# Check if virtual environment exists and remove it
-if [ -d "venv_web_v2" ]; then
- echo "Removing existing virtual environment..."
- rm -rf venv_web_v2
- echo "Virtual environment removed successfully"
-else
- echo "No virtual environment found to remove"
-fi
-
-echo "Cleanup complete!"
diff --git a/scripts/utils/clear_python_cache.sh b/scripts/utils/clear_python_cache.sh
deleted file mode 100755
index 2b9eefba..00000000
--- a/scripts/utils/clear_python_cache.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-# Clear Python cache files that might be causing import issues
-
-echo "🧹 Clearing Python cache files..."
-
-# Clear __pycache__ directories
-find ~/LEDMatrix -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
-
-# Clear .pyc files
-find ~/LEDMatrix -name "*.pyc" -delete 2>/dev/null || true
-
-# Clear Flask session cache if it exists
-rm -rf ~/LEDMatrix/web_interface/.webassets-cache 2>/dev/null || true
-
-echo "✅ Python cache cleared"
-
-echo ""
-echo "🔄 Now try running the web interface again:"
-echo "cd ~/LEDMatrix"
-echo "python3 web_interface/start.py"
diff --git a/scripts/verify_web_ui.sh b/scripts/verify_web_ui.sh
deleted file mode 100755
index 2dfc7628..00000000
--- a/scripts/verify_web_ui.sh
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/bin/bash
-
-# Quick Web UI Verification Script
-# Run this via SSH on your Raspberry Pi to verify the web interface is running
-
-echo "=========================================="
-echo "Web UI Verification"
-echo "=========================================="
-echo ""
-
-# The web interface binds port 5000 (web_interface/start.py).
-WEB_PORT=5000
-PORT_PATTERN=":${WEB_PORT}([^0-9]|$)"
-
-# Colors
-GREEN='\033[0;32m'
-RED='\033[0;31m'
-YELLOW='\033[1;33m'
-NC='\033[0m' # No Color
-
-# 1. Check service status
-echo "1. Checking service status..."
-if systemctl is-active --quiet ledmatrix-web.service 2>/dev/null; then
- echo -e "${GREEN}✓${NC} ledmatrix-web.service is running"
-
- # Get detailed status
- echo ""
- echo "Service details:"
- systemctl status ledmatrix-web.service --no-pager -l | head -15
-else
- echo -e "${RED}✗${NC} ledmatrix-web.service is NOT running"
- echo ""
- echo "To start the service:"
- echo " sudo systemctl start ledmatrix-web.service"
- echo ""
-fi
-echo ""
-
-# 2. Check if port $WEB_PORT is listening
-echo "2. Checking if port $WEB_PORT is listening..."
-if command -v ss >/dev/null 2>&1; then
- if ss -tuln 2>/dev/null | grep -qE "$PORT_PATTERN"; then
- echo -e "${GREEN}✓${NC} Port $WEB_PORT is listening"
- echo ""
- echo "Active connections on port $WEB_PORT:"
- ss -tuln | grep -E "$PORT_PATTERN"
- else
- echo -e "${RED}✗${NC} Port $WEB_PORT is NOT listening"
- fi
-elif command -v netstat >/dev/null 2>&1; then
- if netstat -tuln 2>/dev/null | grep -qE "$PORT_PATTERN"; then
- echo -e "${GREEN}✓${NC} Port $WEB_PORT is listening"
- echo ""
- echo "Active connections on port $WEB_PORT:"
- netstat -tuln | grep -E "$PORT_PATTERN"
- else
- echo -e "${RED}✗${NC} Port $WEB_PORT is NOT listening"
- fi
-else
- echo -e "${YELLOW}⚠${NC} Cannot check port (ss/netstat not available)"
-fi
-echo ""
-
-# 3. Test HTTP connection
-echo "3. Testing HTTP connection..."
-if curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://localhost:$WEB_PORT > /dev/null 2>&1; then
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://localhost:$WEB_PORT 2>/dev/null)
- if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "301" ]; then
- echo -e "${GREEN}✓${NC} Web interface is responding (HTTP $HTTP_CODE)"
- else
- echo -e "${YELLOW}⚠${NC} Web interface responded with HTTP $HTTP_CODE"
- fi
-else
- echo -e "${RED}✗${NC} Cannot connect to web interface on port $WEB_PORT"
-fi
-echo ""
-
-# 4. Get Pi's IP address
-echo "4. Network information..."
-IP_ADDRESSES=$(hostname -I 2>/dev/null | awk '{print $1}')
-if [ -n "$IP_ADDRESSES" ]; then
- echo "Pi IP address(es): $IP_ADDRESSES"
- echo ""
- echo "Access web interface at:"
- for ip in $IP_ADDRESSES; do
- echo " http://$ip:$WEB_PORT"
- done
-else
- echo -e "${YELLOW}⚠${NC} Could not determine IP address"
-fi
-echo ""
-
-# 5. Check recent logs
-echo "5. Recent service logs (last 10 lines)..."
-echo "----------------------------------------"
-journalctl -u ledmatrix-web.service -n 10 --no-pager 2>/dev/null || echo "Could not retrieve logs"
-echo ""
-
-# 6. Check for errors in logs
-echo "6. Checking for errors in logs..."
-ERROR_COUNT=$(journalctl -u ledmatrix-web.service --since "5 minutes ago" --no-pager 2>/dev/null | grep -i "error\|exception\|failed\|traceback" | wc -l)
-if [ "$ERROR_COUNT" -gt 0 ]; then
- echo -e "${YELLOW}⚠${NC} Found $ERROR_COUNT error(s) in last 5 minutes"
- echo ""
- echo "Recent errors:"
- journalctl -u ledmatrix-web.service --since "5 minutes ago" --no-pager 2>/dev/null | grep -i "error\|exception\|failed\|traceback" | tail -5
-else
- echo -e "${GREEN}✓${NC} No errors in recent logs"
-fi
-echo ""
-
-# Summary
-echo "=========================================="
-echo "Summary"
-echo "=========================================="
-
-SERVICE_RUNNING=false
-PORT_LISTENING=false
-HTTP_RESPONDING=false
-
-if systemctl is-active --quiet ledmatrix-web.service 2>/dev/null; then
- SERVICE_RUNNING=true
-fi
-
-if (ss -tuln 2>/dev/null | grep -qE "$PORT_PATTERN") || (netstat -tuln 2>/dev/null | grep -qE "$PORT_PATTERN"); then
- PORT_LISTENING=true
-fi
-
-if curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://localhost:$WEB_PORT > /dev/null 2>&1; then
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://localhost:$WEB_PORT 2>/dev/null)
- if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "301" ]; then
- HTTP_RESPONDING=true
- fi
-fi
-
-if [ "$SERVICE_RUNNING" = true ] && [ "$PORT_LISTENING" = true ] && [ "$HTTP_RESPONDING" = true ]; then
- echo -e "${GREEN}✓ Web UI is running correctly${NC}"
- echo ""
- echo "You can access it at:"
- for ip in $IP_ADDRESSES; do
- echo " http://$ip:$WEB_PORT"
- done
- exit 0
-elif [ "$SERVICE_RUNNING" = false ]; then
- echo -e "${RED}✗ Web UI service is not running${NC}"
- echo ""
- echo "To start it:"
- echo " sudo systemctl start ledmatrix-web.service"
- echo " sudo systemctl enable ledmatrix-web.service # to start on boot"
- exit 1
-elif [ "$PORT_LISTENING" = false ]; then
- echo -e "${RED}✗ Service is running but port $WEB_PORT is not listening${NC}"
- echo ""
- echo "Check logs for errors:"
- echo " sudo journalctl -u ledmatrix-web.service -f"
- exit 1
-else
- echo -e "${YELLOW}⚠ Web UI may have issues${NC}"
- echo ""
- echo "Check logs for details:"
- echo " sudo journalctl -u ledmatrix-web.service -f"
- exit 1
-fi
-
diff --git a/systemd/ledmatrix.service b/systemd/ledmatrix.service
index ab5ddf4d..faaeb9bc 100644
--- a/systemd/ledmatrix.service
+++ b/systemd/ledmatrix.service
@@ -43,11 +43,6 @@ MemoryMax=85%
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ledmatrix
-# Support for on-demand plugin filtering via environment variable
-# The environment variable LEDMATRIX_ON_DEMAND_PLUGIN can be set via:
-# sudo systemctl set-environment LEDMATRIX_ON_DEMAND_PLUGIN=
-# Or by using an EnvironmentFile (see below)
-# EnvironmentFile=__PROJECT_ROOT_DIR__/config/on_demand_env.conf
[Install]
WantedBy=multi-user.target
\ No newline at end of file