chore: second-pass cleanup — dead installer branch, broken script, orphaned JS, misfiled test deps

- first_time_install.sh: removed the pip fallback branch that installed
  from requirements_web_v2.txt — a file that has not existed since the
  v2 web interface was removed (the branch always printed its own
  'not found; skipping' warning).
- scripts/remove_plugin_backups.sh deleted: its PROJECT_ROOT resolved to
  the repo's PARENT directory, and its verify_submodules() checks for
  plugin submodules from an era before plugins moved to the store — it
  could never have worked from its current location.
- plugins_manager.js: removed three functions with zero call sites
  anywhere (addKeyValuePair, formatCommit, togglePasswordVisibility) —
  verified against all templates, all JS, and the dynamic window[name]
  dispatch sites, which resolve widget-registry keys only. Also replaced
  base.html's misleading 'Legacy ... during migration' label: the file
  is deliberately loaded last and provides the LIVE implementations of
  seven window.* plugin actions that shadow same-named definitions in
  app.js/app-shell.js.
- pytest/pytest-cov/pytest-mock moved from runtime requirements.txt to
  requirements-test.txt (CI already installs both files; the installer's
  line-by-line loop simply installs three fewer packages on devices; no
  store plugin declares pytest). HOW_TO_RUN_TESTS.md updated.
- scripts/add_defaults_to_schemas.py and analyze_plugin_schemas.py
  scanned the empty legacy plugins/ dir — now scan plugin-repos/.

Verified: fresh venv installs all four requirements files with pip check
clean and pytest available; bash -n on the installer; node --check on
the JS; widget/cache guard tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXb4mKcAkVaxkeTb3YnAdr
This commit is contained in:
Claude
2026-08-06 01:53:33 +00:00
parent cc1db66662
commit eca41f8daa
9 changed files with 16 additions and 231 deletions
+2 -5
View File
@@ -10,10 +10,7 @@ Make sure you have the testing packages installed:
```bash ```bash
# Install all dependencies including test packages # Install all dependencies including test packages
pip install -r requirements.txt pip install -r requirements.txt -r requirements-test.txt
# Or install just the test dependencies
pip install pytest pytest-cov pytest-mock
``` ```
### 2. Set Environment Variables ### 2. Set Environment Variables
@@ -302,7 +299,7 @@ If tests fail due to missing packages:
```bash ```bash
# Install all dependencies # Install all dependencies
pip install -r requirements.txt pip install -r requirements.txt -r requirements-test.txt
# Or install specific missing package # Or install specific missing package
pip install <package-name> pip install <package-name>
+1 -8
View File
@@ -1189,14 +1189,7 @@ else
# real time and in order relative to this script's own echo statements # real time and in order relative to this script's own echo statements
python3 -u "$PROJECT_ROOT_DIR/scripts/install_dependencies_apt.py" python3 -u "$PROJECT_ROOT_DIR/scripts/install_dependencies_apt.py"
else else
echo "Using pip to install dependencies..." echo "Web dependencies already installed from web_interface/requirements.txt in Step 5"
if [ -f "$PROJECT_ROOT_DIR/requirements_web_v2.txt" ]; then
# --ignore-installed: see the Step 5 web_interface/requirements.txt
# install above — same apt/pip RECORD-file conflict applies here.
python3 -m pip install --break-system-packages --prefer-binary --ignore-installed -r requirements_web_v2.txt
else
echo "⚠ requirements_web_v2.txt not found; skipping web dependency install"
fi
fi fi
# Create marker file to indicate dependencies are installed # Create marker file to indicate dependencies are installed
+4 -7
View File
@@ -1,10 +1,7 @@
# Test-only dependencies for the plugin safety harness and pytest suite. # Test/dev-only dependencies (not needed on a running display).
# Install alongside requirements.txt: pip install -r requirements.txt -r requirements-test.txt # Install alongside requirements.txt: pip install -r requirements.txt -r requirements-test.txt
# pytest>=9.0.3,<10.0.0
# pytest, pytest-cov, pytest-mock, and jsonschema are already pinned (with pytest-cov>=4.1.0,<5.0.0
# major-version caps) in requirements.txt, so they are intentionally NOT pytest-mock>=3.11.0,<4.0.0
# repeated here — re-pinning pytest to <9 collided with requirements.txt's
# pytest>=9.0.3,<10 and made the two files impossible to install together.
# Only declare what requirements.txt doesn't already provide.
freezegun>=1.2,<2 # deterministic time for golden-image tests freezegun>=1.2,<2 # deterministic time for golden-image tests
mypy>=1.5.0,<2.0.0 # static type checking (also pinned in .pre-commit-config.yaml) mypy>=1.5.0,<2.0.0 # static type checking (also pinned in .pre-commit-config.yaml)
+2 -4
View File
@@ -34,10 +34,8 @@ jsonschema>=4.20.0,<5.0.0
# Requirement specifier parsing (plugin dependency satisfaction checks) # Requirement specifier parsing (plugin dependency satisfaction checks)
packaging>=23.0,<27.0 packaging>=23.0,<27.0
# Testing dependencies # Testing dependencies live in requirements-test.txt:
pytest>=9.0.3,<10.0.0 # pip install -r requirements.txt -r requirements-test.txt
pytest-cov>=4.1.0,<5.0.0
pytest-mock>=3.11.0,<4.0.0
# ─────────────────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────────────────
# Optional dependencies — the code imports these inside try/except # Optional dependencies — the code imports these inside try/except
+1 -1
View File
@@ -201,7 +201,7 @@ def process_schema_file(schema_path: Path) -> bool:
def main(): def main():
"""Main entry point.""" """Main entry point."""
project_root = Path(__file__).parent.parent project_root = Path(__file__).parent.parent
plugins_dir = project_root / 'plugins' plugins_dir = project_root / 'plugin-repos'
if not plugins_dir.exists(): if not plugins_dir.exists():
print(f"Error: Plugins directory not found: {plugins_dir}") print(f"Error: Plugins directory not found: {plugins_dir}")
+1 -1
View File
@@ -193,7 +193,7 @@ def analyze_schema(schema_path: Path) -> Dict[str, Any]:
def main(): def main():
"""Main analysis function.""" """Main analysis function."""
project_root = Path(__file__).parent.parent project_root = Path(__file__).parent.parent
plugins_dir = project_root / "plugins" plugins_dir = project_root / "plugin-repos"
if not plugins_dir.exists(): if not plugins_dir.exists():
print(f"Plugins directory not found: {plugins_dir}") print(f"Plugins directory not found: {plugins_dir}")
-117
View File
@@ -1,117 +0,0 @@
#!/bin/bash
# Script to safely remove plugin backup directories
# These were created during the plugin-to-submodule conversion
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
PLUGINS_DIR="$PROJECT_ROOT/plugins"
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Verify submodules are working
verify_submodules() {
log_info "Verifying submodules are working..."
local issues=0
for submod in football-scoreboard hockey-scoreboard ledmatrix-flights \
ledmatrix-leaderboard ledmatrix-stocks ledmatrix-weather \
mqtt-notifications; do
if [ ! -d "$PLUGINS_DIR/$submod" ]; then
log_error "Submodule directory missing: $submod"
issues=$((issues + 1))
elif [ ! -f "$PLUGINS_DIR/$submod/.git" ]; then
log_error "Submodule .git file missing: $submod"
issues=$((issues + 1))
elif [ ! -f "$PLUGINS_DIR/$submod/manifest.json" ]; then
log_warn "Submodule manifest missing: $submod (may be OK)"
fi
done
if [ $issues -eq 0 ]; then
log_info "All submodules verified ✓"
return 0
else
log_error "Found $issues issues with submodules"
return 1
fi
}
# Remove backup directories
remove_backups() {
log_info "Removing backup directories..."
local removed=0
local total_size=0
for backup in "$PLUGINS_DIR"/*.backup*; do
if [ -d "$backup" ]; then
local name=$(basename "$backup")
local size=$(du -sb "$backup" 2>/dev/null | awk '{print $1}')
total_size=$((total_size + size))
log_info "Removing: $name"
rm -rf "$backup"
removed=$((removed + 1))
fi
done
if [ $removed -gt 0 ]; then
log_info "Removed $removed backup directory(ies)"
log_info "Freed approximately $(numfmt --to=iec-i --suffix=B $total_size 2>/dev/null || echo "$total_size bytes")"
else
log_info "No backup directories found"
fi
}
# Main
main() {
cd "$PROJECT_ROOT"
echo "=== Plugin Backup Removal Script ==="
echo
# Verify submodules first
if ! verify_submodules; then
log_error "Submodule verification failed. Not removing backups."
log_warn "Please fix submodule issues before removing backups."
exit 1
fi
echo
log_warn "This will permanently delete backup directories:"
ls -1d "$PLUGINS_DIR"/*.backup* 2>/dev/null | sed 's|.*/| - |' || echo " (none found)"
echo
read -p "Continue? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Aborted"
exit 0
fi
remove_backups
log_info "Done!"
}
main "$@"
@@ -2299,56 +2299,6 @@ function renderArrayObjectItem(fieldId, fullKey, itemProperties, itemValue, inde
// Functions to handle patternProperties key-value pairs // Functions to handle patternProperties key-value pairs
window.addKeyValuePair = function(fieldId, fullKey, maxProperties) {
const pairsContainer = document.getElementById(fieldId + '_pairs');
if (!pairsContainer) return;
const currentPairs = pairsContainer.querySelectorAll('.key-value-pair');
if (currentPairs.length >= maxProperties) {
alert(`Maximum ${maxProperties} entries allowed`);
return;
}
const newIndex = currentPairs.length;
const valueType = 'string'; // Default to string, could be determined from schema
const pairHtml = `
<div class="flex items-center gap-2 key-value-pair" data-index="${newIndex}">
<input type="text"
name="${fullKey}[key_${newIndex}]"
value=""
placeholder="Key"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
data-key-index="${newIndex}"
onchange="updateKeyValuePairData('${fieldId}', '${fullKey}')">
<input type="text"
name="${fullKey}[value_${newIndex}]"
value=""
placeholder="Value"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
data-value-index="${newIndex}"
onchange="updateKeyValuePairData('${fieldId}', '${fullKey}')">
<button type="button"
onclick="removeKeyValuePair('${fieldId}', ${newIndex})"
class="px-3 py-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md transition-colors"
title="Remove">
<i class="fas fa-trash"></i>
</button>
</div>
`;
pairsContainer.insertAdjacentHTML('beforeend', pairHtml);
updateKeyValuePairData(fieldId, fullKey);
// Update add button state
const addButton = pairsContainer.nextElementSibling;
if (addButton && currentPairs.length + 1 >= maxProperties) {
addButton.disabled = true;
addButton.style.opacity = '0.5';
addButton.style.cursor = 'not-allowed';
}
};
window.removeKeyValuePair = function(fieldId, index) { window.removeKeyValuePair = function(fieldId, index) {
const pairsContainer = document.getElementById(fieldId + '_pairs'); const pairsContainer = document.getElementById(fieldId + '_pairs');
if (!pairsContainer) return; if (!pairsContainer) return;
@@ -4559,23 +4509,6 @@ function formatDate(dateString) {
} }
} }
function formatCommit(commit, branch) {
const shortCommit = commit ? String(commit).substring(0, 7) : '';
const branchText = branch ? String(branch) : '';
if (branchText && shortCommit) {
return `${branchText} · ${shortCommit}`;
}
if (branchText) {
return branchText;
}
if (shortCommit) {
return shortCommit;
}
return 'Latest';
}
// Check if plugin is new (updated within last 7 days)
function isNewPlugin(lastUpdated) { function isNewPlugin(lastUpdated) {
if (!lastUpdated) return false; if (!lastUpdated) return false;
@@ -4605,26 +4538,6 @@ function debounce(func, wait) {
} }
// Toggle password visibility for secret fields // Toggle password visibility for secret fields
function togglePasswordVisibility(fieldId) {
const input = document.getElementById(fieldId);
const icon = document.getElementById(fieldId + '-icon');
if (input && icon) {
if (input.type === 'password') {
input.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
input.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
}
// GitHub Token Configuration Functions
// Open GitHub Token Settings panel (only opens, doesn't close)
// Used when user clicks "Configure Token" link
window.openGithubTokenSettings = function() { window.openGithubTokenSettings = function() {
const settings = document.getElementById('github-token-settings'); const settings = document.getElementById('github-token-settings');
const warning = document.getElementById('github-auth-warning'); const warning = document.getElementById('github-auth-warning');
+5 -1
View File
@@ -1013,7 +1013,11 @@
<!-- Reusable JSON file manager widget (used by of-the-day and others via x-widget: json-file-manager) --> <!-- Reusable JSON file manager widget (used by of-the-day and others via x-widget: json-file-manager) -->
<script src="{{ url_for('static', filename='v3/js/widgets/json-file-manager.js') }}" defer></script> <script src="{{ url_for('static', filename='v3/js/widgets/json-file-manager.js') }}" defer></script>
<!-- Legacy plugins_manager.js (for backward compatibility during migration) --> <!-- plugins_manager.js: loaded LAST on purpose — it defines the live
window.* implementations for several plugin actions (installPlugin,
uninstallPlugin, executePluginAction, ...) and intentionally wins
over same-named definitions in js/app.js and js/app-shell.js. Do not
remove or reorder without resolving that overlap first. -->
<script src="{{ url_for('static', filename='v3/plugins_manager.js') }}?v=20260307" defer></script> <script src="{{ url_for('static', filename='v3/plugins_manager.js') }}?v=20260307" defer></script>
<!-- Custom feeds table helpers live in js/widgets/custom-feeds.js (the <!-- Custom feeds table helpers live in js/widgets/custom-feeds.js (the