diff --git a/docs/MIGRATION_GUIDE.md b/docs/MIGRATION_GUIDE.md
index c6b17c94..b768c82b 100644
--- a/docs/MIGRATION_GUIDE.md
+++ b/docs/MIGRATION_GUIDE.md
@@ -19,7 +19,6 @@ All installation scripts have been moved from the project root to `scripts/insta
| `install_wifi_monitor.sh` | `scripts/install/install_wifi_monitor.sh` |
| `setup_cache.sh` | `scripts/install/setup_cache.sh` |
| `configure_web_sudo.sh` | `scripts/install/configure_web_sudo.sh` |
-| `migrate_config.sh` | `scripts/install/migrate_config.sh` |
#### Permission Fix Scripts
diff --git a/docs/MULTI_ROOT_WORKSPACE_SETUP.md b/docs/MULTI_ROOT_WORKSPACE_SETUP.md
index 56bbab2f..17e0fe09 100644
--- a/docs/MULTI_ROOT_WORKSPACE_SETUP.md
+++ b/docs/MULTI_ROOT_WORKSPACE_SETUP.md
@@ -10,11 +10,12 @@ Official plugins live in a single repository,
[ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins), with one
directory per plugin under `plugins/`. There are no separate per-plugin
repositories. For development you clone that monorepo **next to** LEDMatrix
-and symlink its plugin directories into LEDMatrix's `plugin-repos/`, which is
-where the plugin loader looks by default.
+and symlink the plugin directories you are working on into LEDMatrix's
+`plugins/` directory with `scripts/dev/dev_plugin_setup.sh`.
- ✅ Plugin code stays in the monorepo checkout, with its own git history
-- ✅ LEDMatrix discovers the plugins through symlinks in `plugin-repos/`
+- ✅ LEDMatrix discovers the plugins through symlinks in `plugins/`
+ (git-ignored), so the production `plugin-repos/` directory is untouched
- ✅ `LEDMatrix.code-workspace` opens both repositories in VS Code/Cursor
## Directory Structure
@@ -22,12 +23,11 @@ where the plugin loader looks by default.
```text
~/Github/
├── LEDMatrix/ # Main project
-│ ├── plugin-repos/ # Plugin directory the loader scans
-│ │ ├── starlark-apps/ # Bundled with LEDMatrix (tracked in git)
-│ │ ├── web-ui-info/ # Bundled with LEDMatrix (tracked in git)
-│ │ ├── clock-simple -> ../../ledmatrix-plugins/plugins/clock-simple
-│ │ ├── ledmatrix-weather -> ../../ledmatrix-plugins/plugins/ledmatrix-weather
+│ ├── plugins/ # Dev plugin directory (git-ignored)
+│ │ ├── clock-simple -> ~/Github/ledmatrix-plugins/plugins/clock-simple
+│ │ ├── ledmatrix-weather -> ~/Github/ledmatrix-plugins/plugins/ledmatrix-weather
│ │ └── ...
+│ ├── plugin-repos/ # Default (Plugin Store) plugin directory
│ ├── LEDMatrix.code-workspace # Opens LEDMatrix and ../ledmatrix-plugins
│ └── ...
└── ledmatrix-plugins/ # Plugin monorepo (git repo)
@@ -44,61 +44,65 @@ where the plugin loader looks by default.
### 1. The plugin monorepo
Clone ledmatrix-plugins into the same parent directory as LEDMatrix (the
-scripts below look for `../ledmatrix-plugins` relative to the LEDMatrix
-root):
+workspace file and `scripts/update_plugin_repos.py` look for
+`../ledmatrix-plugins` relative to the LEDMatrix root):
```bash
cd ~/Github
git clone https://github.com/ChuckBuilds/ledmatrix-plugins.git
```
-### 2. Symlinks in plugin-repos/
+### 2. Symlinks in plugins/
-`scripts/setup_plugin_repos.py` creates one symlink per plugin in
-`LEDMatrix/plugin-repos/`, named after the plugin's manifest `id` and pointing
-at `../ledmatrix-plugins/plugins/
`.
+`scripts/dev/dev_plugin_setup.sh link ` creates
+`LEDMatrix/plugins/` as a symlink to a plugin directory. Use the
+plugin's manifest `id` as the name: that is the name the loader and
+`config.json` use, and the script warns when the two differ.
### 3. Multi-root workspace
`LEDMatrix.code-workspace` has two roots: LEDMatrix itself and
`../ledmatrix-plugins`.
-## Setup Scripts
+## Setup
-### Initial Setup
+### Link plugins
```bash
cd ~/Github/LEDMatrix
-python3 scripts/setup_plugin_repos.py
+./scripts/dev/dev_plugin_setup.sh link clock-simple ../ledmatrix-plugins/plugins/clock-simple
+./scripts/dev/dev_plugin_setup.sh list # show what is linked
```
-This script:
-- Reads each `manifest.json` under `../ledmatrix-plugins/plugins/`
-- Creates `plugin-repos/` symlinks (relative) to those directories
-- Leaves correct links alone, replaces links that point elsewhere, and skips
- (does not overwrite) a real directory of the same name — for example a
- plugin you installed from the Plugin Store. Remove that directory first if
- you want the linked copy.
+If a real (non-symlink) directory of the same name already exists in
+`plugins/`, the script offers to back it up and replace it.
+
+Without a sibling checkout, `./scripts/dev/dev_plugin_setup.sh link-github
+` clones the monorepo into `~/.ledmatrix-dev-plugins/` instead and links
+the plugin from there. See the
+[Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md).
### Updating Plugins
```bash
cd ~/Github/LEDMatrix
-python3 scripts/update_plugin_repos.py
+python3 scripts/update_plugin_repos.py # git pull in ../ledmatrix-plugins
+# or
+./scripts/dev/dev_plugin_setup.sh update # git pull in every linked checkout
```
-This runs `git pull` in `../ledmatrix-plugins` and prints the result. The
-symlinks pick up the new code; restart the display to load it.
+The symlinks pick up the new code; restart the display to load it.
## Configuration
-The loader reads plugins from `plugin_system.plugins_directory` in
-`config/config.json`. The default is already right for this setup:
+The loader scans only `plugin_system.plugins_directory` in
+`config/config.json` (default `plugin-repos`). Point it at `plugins` so it
+finds the links:
```json
{
"plugin_system": {
- "plugins_directory": "plugin-repos"
+ "plugins_directory": "plugins"
}
}
```
@@ -117,7 +121,7 @@ The loader reads plugins from `plugin_system.plugins_directory` in
### Adding New Plugins
1. Create `plugins//` in the monorepo checkout
-2. Run `python3 scripts/setup_plugin_repos.py` in LEDMatrix to link it
+2. Link it: `./scripts/dev/dev_plugin_setup.sh link ../ledmatrix-plugins/plugins/`
## Troubleshooting
@@ -125,30 +129,21 @@ The loader reads plugins from `plugin_system.plugins_directory` in
```bash
cd ~/Github/LEDMatrix
-ls -la plugin-repos/ # links present and not broken?
-python3 scripts/setup_plugin_repos.py # recreate them
+ls -la plugins/ # links present and not broken?
+./scripts/dev/dev_plugin_setup.sh status # link targets and git state
```
-Also check that `plugin_system.plugins_directory` is `plugin-repos`.
-
-### "Monorepo plugins directory not found"
-
-`setup_plugin_repos.py` expects the monorepo at `../ledmatrix-plugins`. Clone
-it there (or symlink it there).
+Also check that `plugin_system.plugins_directory` is `plugins`.
### Plugin updates not showing
-1. Verify the link target: `ls -la plugin-repos/`
+1. Verify the link target: `ls -la plugins/`
2. Check that you're editing the monorepo checkout, not a store-installed copy
3. Restart the LEDMatrix service (or `run.py`)
## Notes
-- `plugin-repos/` is tracked in git only for the bundled plugins
- (`starlark-apps`, `web-ui-info`). The symlinks you create are untracked
- files; don't commit them.
-- For linking a single plugin into `plugins/` instead (without a sibling
- checkout), see `scripts/dev/dev_plugin_setup.sh` in the
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md).
+- `plugins/` is git-ignored (except `plugins/.gitkeep`); the symlinks are
+ never committed.
- When changing a plugin in the monorepo, bump its manifest `version` and run
`python update_registry.py`, or users won't receive the update.
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
-