mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-06 11:18:06 +00:00
- 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
232 lines
6.8 KiB
Python
Executable File
232 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Script to add default values to plugin config schemas where missing.
|
|
|
|
This ensures that configs never start with None values, improving user experience
|
|
and preventing validation errors.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
def get_default_for_field(prop: Dict[str, Any]) -> Any:
|
|
"""
|
|
Determine a sensible default value for a field based on its type and constraints.
|
|
|
|
Args:
|
|
prop: Field property schema
|
|
|
|
Returns:
|
|
Default value or None if no default should be added
|
|
"""
|
|
prop_type = prop.get('type')
|
|
|
|
# Handle union types (array with multiple types)
|
|
if isinstance(prop_type, list):
|
|
# Use the first non-null type
|
|
prop_type = next((t for t in prop_type if t != 'null'), prop_type[0] if prop_type else 'string')
|
|
|
|
if prop_type == 'boolean':
|
|
return False
|
|
|
|
elif prop_type == 'number':
|
|
# For numbers, use minimum if available, or a sensible default
|
|
minimum = prop.get('minimum')
|
|
maximum = prop.get('maximum')
|
|
|
|
if minimum is not None:
|
|
return minimum
|
|
elif maximum is not None:
|
|
# Use a reasonable fraction of max (like 30% or minimum 1)
|
|
return max(1, int(maximum * 0.3))
|
|
else:
|
|
# No constraints, use 0
|
|
return 0
|
|
|
|
elif prop_type == 'integer':
|
|
# Similar to number
|
|
minimum = prop.get('minimum')
|
|
maximum = prop.get('maximum')
|
|
|
|
if minimum is not None:
|
|
return minimum
|
|
elif maximum is not None:
|
|
return max(1, int(maximum * 0.3))
|
|
else:
|
|
return 0
|
|
|
|
elif prop_type == 'string':
|
|
# Only add default for strings if it makes sense
|
|
# Check if there's an enum - use first value
|
|
enum_values = prop.get('enum')
|
|
if enum_values:
|
|
return enum_values[0]
|
|
|
|
# For optional string fields, empty string might be okay, but be cautious
|
|
# We'll skip adding defaults for strings unless explicitly needed
|
|
return None
|
|
|
|
elif prop_type == 'array':
|
|
# Empty array as default
|
|
return []
|
|
|
|
elif prop_type == 'object':
|
|
# Empty object - but we'll handle nested objects separately
|
|
return {}
|
|
|
|
return None
|
|
|
|
|
|
def should_add_default(prop: Dict[str, Any], field_path: str) -> bool:
|
|
"""
|
|
Determine if we should add a default value to this field.
|
|
|
|
Args:
|
|
prop: Field property schema
|
|
field_path: Dot-separated path to the field
|
|
|
|
Returns:
|
|
True if default should be added
|
|
"""
|
|
# Skip if already has a default
|
|
if 'default' in prop:
|
|
return False
|
|
|
|
# Skip secret fields (they should be user-provided)
|
|
if prop.get('x-secret', False):
|
|
return False
|
|
|
|
# Skip API keys and similar sensitive fields
|
|
field_name = field_path.split('.')[-1].lower()
|
|
sensitive_keywords = ['key', 'password', 'secret', 'token', 'auth', 'credential']
|
|
if any(keyword in field_name for keyword in sensitive_keywords):
|
|
return False
|
|
|
|
prop_type = prop.get('type')
|
|
if isinstance(prop_type, list):
|
|
prop_type = next((t for t in prop_type if t != 'null'), prop_type[0] if prop_type else None)
|
|
|
|
# Only add defaults for certain types
|
|
if prop_type in ('boolean', 'number', 'integer', 'array'):
|
|
return True
|
|
|
|
# For strings, only if there's an enum
|
|
if prop_type == 'string' and 'enum' in prop:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def add_defaults_recursive(schema: Dict[str, Any], path: str = "", modified: List[str] = None) -> bool:
|
|
"""
|
|
Recursively add default values to schema fields.
|
|
|
|
Args:
|
|
schema: Schema dictionary to modify
|
|
path: Current path in the schema (for logging)
|
|
modified: List to track which fields were modified
|
|
|
|
Returns:
|
|
True if any modifications were made
|
|
"""
|
|
if modified is None:
|
|
modified = []
|
|
|
|
if not isinstance(schema, dict) or 'properties' not in schema:
|
|
return False
|
|
|
|
changes_made = False
|
|
|
|
for key, prop in schema['properties'].items():
|
|
if not isinstance(prop, dict):
|
|
continue
|
|
|
|
current_path = f"{path}.{key}" if path else key
|
|
|
|
# Check nested objects
|
|
if prop.get('type') == 'object' and 'properties' in prop:
|
|
if add_defaults_recursive(prop, current_path, modified):
|
|
changes_made = True
|
|
|
|
# Add default if appropriate
|
|
if should_add_default(prop, current_path):
|
|
default_value = get_default_for_field(prop)
|
|
if default_value is not None:
|
|
prop['default'] = default_value
|
|
modified.append(current_path)
|
|
changes_made = True
|
|
print(f" Added default to {current_path}: {default_value} (type: {prop.get('type')})")
|
|
|
|
return changes_made
|
|
|
|
|
|
def process_schema_file(schema_path: Path) -> bool:
|
|
"""
|
|
Process a single schema file to add defaults.
|
|
|
|
Args:
|
|
schema_path: Path to the schema file
|
|
|
|
Returns:
|
|
True if file was modified
|
|
"""
|
|
print(f"\nProcessing: {schema_path}")
|
|
|
|
try:
|
|
with open(schema_path, 'r', encoding='utf-8') as f:
|
|
schema = json.load(f)
|
|
except Exception as e:
|
|
print(f" Error reading schema: {e}")
|
|
return False
|
|
|
|
modified_fields = []
|
|
changes_made = add_defaults_recursive(schema, modified=modified_fields)
|
|
|
|
if changes_made:
|
|
# Write back with pretty formatting
|
|
with open(schema_path, 'w', encoding='utf-8') as f:
|
|
json.dump(schema, f, indent=2, ensure_ascii=False)
|
|
f.write('\n') # Add trailing newline
|
|
|
|
print(f" ✓ Modified {len(modified_fields)} fields")
|
|
return True
|
|
else:
|
|
print(f" ✓ No changes needed")
|
|
return False
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
project_root = Path(__file__).parent.parent
|
|
plugins_dir = project_root / 'plugin-repos'
|
|
|
|
if not plugins_dir.exists():
|
|
print(f"Error: Plugins directory not found: {plugins_dir}")
|
|
sys.exit(1)
|
|
|
|
# Find all config_schema.json files
|
|
schema_files = list(plugins_dir.rglob('config_schema.json'))
|
|
|
|
if not schema_files:
|
|
print("No config_schema.json files found")
|
|
sys.exit(0)
|
|
|
|
print(f"Found {len(schema_files)} schema files")
|
|
|
|
modified_count = 0
|
|
for schema_file in sorted(schema_files):
|
|
if process_schema_file(schema_file):
|
|
modified_count += 1
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"Summary: Modified {modified_count} out of {len(schema_files)} schema files")
|
|
print(f"{'='*60}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|