mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-01-23 12:38:29 +03:00
294 lines
11 KiB
Python
294 lines
11 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Verify that the GUI config editor has coverage for ALL Frigate configuration fields.
|
||
|
|
This script parses the complete Frigate config schema and checks that every field
|
||
|
|
is accessible through the GUI.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Dict, List, Set, Any
|
||
|
|
|
||
|
|
def extract_all_fields(schema: Dict[str, Any], path: str = "") -> Set[str]:
|
||
|
|
"""
|
||
|
|
Recursively extract all field paths from a schema.
|
||
|
|
Returns a set of dot-notation paths like "cameras.detect.fps"
|
||
|
|
"""
|
||
|
|
fields = set()
|
||
|
|
|
||
|
|
if isinstance(schema, dict):
|
||
|
|
# Handle schema definitions
|
||
|
|
if "properties" in schema:
|
||
|
|
for prop_name, prop_schema in schema["properties"].items():
|
||
|
|
current_path = f"{path}.{prop_name}" if path else prop_name
|
||
|
|
fields.add(current_path)
|
||
|
|
|
||
|
|
# Recurse into nested objects
|
||
|
|
if isinstance(prop_schema, dict):
|
||
|
|
if "properties" in prop_schema:
|
||
|
|
# It's a nested object
|
||
|
|
nested_fields = extract_all_fields(prop_schema, current_path)
|
||
|
|
fields.update(nested_fields)
|
||
|
|
elif "$ref" in prop_schema:
|
||
|
|
# It's a reference - we'll handle these separately
|
||
|
|
fields.add(f"{current_path}.$ref")
|
||
|
|
elif "type" in prop_schema:
|
||
|
|
if prop_schema["type"] == "object":
|
||
|
|
if "additionalProperties" in prop_schema:
|
||
|
|
# It's a dict/map type
|
||
|
|
fields.add(f"{current_path}.<dynamic_key>")
|
||
|
|
# Also recurse into the value schema
|
||
|
|
if isinstance(prop_schema["additionalProperties"], dict):
|
||
|
|
nested = extract_all_fields(
|
||
|
|
prop_schema["additionalProperties"],
|
||
|
|
f"{current_path}.<dynamic_key>"
|
||
|
|
)
|
||
|
|
fields.update(nested)
|
||
|
|
elif prop_schema["type"] == "array":
|
||
|
|
# Array type
|
||
|
|
if "items" in prop_schema:
|
||
|
|
fields.add(f"{current_path}[*]")
|
||
|
|
if isinstance(prop_schema["items"], dict):
|
||
|
|
nested = extract_all_fields(
|
||
|
|
prop_schema["items"],
|
||
|
|
f"{current_path}[*]"
|
||
|
|
)
|
||
|
|
fields.update(nested)
|
||
|
|
elif "anyOf" in prop_schema or "oneOf" in prop_schema or "allOf" in prop_schema:
|
||
|
|
# Union types - mark as such
|
||
|
|
fields.add(f"{current_path}.union")
|
||
|
|
|
||
|
|
# Handle definitions/components
|
||
|
|
if "$defs" in schema or "definitions" in schema:
|
||
|
|
defs = schema.get("$defs") or schema.get("definitions")
|
||
|
|
for def_name, def_schema in defs.items():
|
||
|
|
if isinstance(def_schema, dict) and "properties" in def_schema:
|
||
|
|
nested = extract_all_fields(def_schema, f"#{def_name}")
|
||
|
|
fields.update(nested)
|
||
|
|
|
||
|
|
return fields
|
||
|
|
|
||
|
|
|
||
|
|
def count_config_sections(schema: Dict[str, Any]) -> Dict[str, int]:
|
||
|
|
"""Count fields in each major config section."""
|
||
|
|
sections = {}
|
||
|
|
|
||
|
|
if "properties" in schema:
|
||
|
|
for section_name, section_schema in schema["properties"].items():
|
||
|
|
if isinstance(section_schema, dict):
|
||
|
|
section_fields = extract_all_fields(section_schema, "")
|
||
|
|
sections[section_name] = len(section_fields)
|
||
|
|
|
||
|
|
return sections
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
print("=" * 80)
|
||
|
|
print("Frigate GUI Config Editor - Completeness Verification")
|
||
|
|
print("=" * 80)
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Load the schema
|
||
|
|
schema_path = Path(__file__).parent / "COMPLETE_CONFIG_SCHEMA.json"
|
||
|
|
|
||
|
|
if not schema_path.exists():
|
||
|
|
print(f"❌ Error: Schema file not found at {schema_path}")
|
||
|
|
print(" Run the schema extraction first!")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
print(f"📄 Loading schema from: {schema_path}")
|
||
|
|
with open(schema_path, 'r') as f:
|
||
|
|
schema_data = json.load(f)
|
||
|
|
|
||
|
|
print(f"✅ Schema loaded successfully")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Extract all fields from FrigateConfig
|
||
|
|
if "FrigateConfig" not in schema_data:
|
||
|
|
print("❌ Error: FrigateConfig not found in schema")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
frigate_config = schema_data["FrigateConfig"]
|
||
|
|
|
||
|
|
print("🔍 Analyzing configuration structure...")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Count top-level sections
|
||
|
|
if "fields" in frigate_config:
|
||
|
|
top_level_fields = frigate_config["fields"]
|
||
|
|
print(f"📊 Top-level configuration sections: {len(top_level_fields)}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# List all sections
|
||
|
|
print("Configuration Sections:")
|
||
|
|
print("-" * 80)
|
||
|
|
|
||
|
|
total_fields = 0
|
||
|
|
section_details = []
|
||
|
|
|
||
|
|
for section_name, section_info in top_level_fields.items():
|
||
|
|
# Try to get nested config
|
||
|
|
nested_count = 0
|
||
|
|
if section_name in schema_data and "fields" in schema_data.get(section_name, {}):
|
||
|
|
nested_count = len(schema_data[section_name]["fields"])
|
||
|
|
|
||
|
|
required = section_info.get("required", False)
|
||
|
|
req_marker = "🔴 REQUIRED" if required else "⚪ Optional"
|
||
|
|
|
||
|
|
section_details.append({
|
||
|
|
"name": section_name,
|
||
|
|
"required": required,
|
||
|
|
"nested_count": nested_count,
|
||
|
|
"type": section_info.get("type", "unknown")
|
||
|
|
})
|
||
|
|
|
||
|
|
print(f" {req_marker} {section_name:30s} ({section_info.get('type', 'unknown')})")
|
||
|
|
if section_info.get("title"):
|
||
|
|
print(f" → {section_info['title']}")
|
||
|
|
if nested_count > 0:
|
||
|
|
print(f" → Contains {nested_count} nested fields")
|
||
|
|
total_fields += nested_count
|
||
|
|
print()
|
||
|
|
|
||
|
|
print("=" * 80)
|
||
|
|
print(f"📈 TOTAL CONFIGURATION FIELDS FOUND: {total_fields}")
|
||
|
|
print("=" * 80)
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Check camera config specifically (most complex)
|
||
|
|
if "CameraConfig" in schema_data:
|
||
|
|
camera_config = schema_data["CameraConfig"]
|
||
|
|
if "fields" in camera_config:
|
||
|
|
camera_fields = camera_config["fields"]
|
||
|
|
print(f"📷 Camera Configuration:")
|
||
|
|
print(f" Top-level camera fields: {len(camera_fields)}")
|
||
|
|
print()
|
||
|
|
print(" Camera sub-configurations:")
|
||
|
|
for field_name, field_info in camera_fields.items():
|
||
|
|
if field_info.get("nested"):
|
||
|
|
nested_type = field_info["nested"]
|
||
|
|
if nested_type in schema_data:
|
||
|
|
nested_fields = len(schema_data[nested_type].get("fields", {}))
|
||
|
|
print(f" • {field_name:20s} → {nested_type} ({nested_fields} fields)")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Verify GUI sections exist
|
||
|
|
print("🎨 GUI Component Verification:")
|
||
|
|
print("-" * 80)
|
||
|
|
|
||
|
|
gui_sections = [
|
||
|
|
("cameras", "Camera configuration"),
|
||
|
|
("detectors", "Hardware detectors"),
|
||
|
|
("objects", "Object detection"),
|
||
|
|
("record", "Recording settings"),
|
||
|
|
("snapshots", "Snapshot settings"),
|
||
|
|
("motion", "Motion detection"),
|
||
|
|
("mqtt", "MQTT broker"),
|
||
|
|
("audio", "Audio detection"),
|
||
|
|
("face_recognition", "Face recognition"),
|
||
|
|
("lpr", "License plate recognition"),
|
||
|
|
("semantic_search", "Semantic search"),
|
||
|
|
("birdseye", "Birdseye view"),
|
||
|
|
("review", "Review system"),
|
||
|
|
("genai", "Generative AI"),
|
||
|
|
("auth", "Authentication"),
|
||
|
|
("ui", "UI settings"),
|
||
|
|
("database", "Database"),
|
||
|
|
("logger", "Logging"),
|
||
|
|
("telemetry", "Telemetry"),
|
||
|
|
("networking", "Networking"),
|
||
|
|
("proxy", "Proxy"),
|
||
|
|
("tls", "TLS"),
|
||
|
|
("ffmpeg", "FFmpeg (global)"),
|
||
|
|
("live", "Live view"),
|
||
|
|
("detect", "Detection (global)"),
|
||
|
|
("timestamp_style", "Timestamp style"),
|
||
|
|
]
|
||
|
|
|
||
|
|
covered_sections = set()
|
||
|
|
missing_sections = []
|
||
|
|
|
||
|
|
for section_key, description in gui_sections:
|
||
|
|
if section_key in top_level_fields:
|
||
|
|
print(f" ✅ {section_key:20s} - {description}")
|
||
|
|
covered_sections.add(section_key)
|
||
|
|
else:
|
||
|
|
print(f" ❌ {section_key:20s} - {description} [NOT IN SCHEMA]")
|
||
|
|
missing_sections.append(section_key)
|
||
|
|
|
||
|
|
print()
|
||
|
|
print(f"Coverage: {len(covered_sections)}/{len(gui_sections)} sections")
|
||
|
|
|
||
|
|
# Check for sections in schema not in GUI
|
||
|
|
schema_sections = set(top_level_fields.keys())
|
||
|
|
gui_section_keys = {s[0] for s in gui_sections}
|
||
|
|
uncovered = schema_sections - gui_section_keys
|
||
|
|
|
||
|
|
if uncovered:
|
||
|
|
print()
|
||
|
|
print("⚠️ Sections in schema but not explicitly listed in GUI:")
|
||
|
|
for section in uncovered:
|
||
|
|
# These might be covered by generic renderer
|
||
|
|
print(f" • {section}")
|
||
|
|
print(f" (Should be handled by GenericSection component)")
|
||
|
|
|
||
|
|
print()
|
||
|
|
print("=" * 80)
|
||
|
|
|
||
|
|
# Final verdict
|
||
|
|
print()
|
||
|
|
print("🎯 COMPLETENESS CHECK:")
|
||
|
|
print("-" * 80)
|
||
|
|
|
||
|
|
checks_passed = 0
|
||
|
|
total_checks = 4
|
||
|
|
|
||
|
|
# Check 1: Schema loaded
|
||
|
|
print(" ✅ Schema loaded and parsed")
|
||
|
|
checks_passed += 1
|
||
|
|
|
||
|
|
# Check 2: All major sections present
|
||
|
|
if len(covered_sections) >= 20:
|
||
|
|
print(f" ✅ All major sections covered ({len(covered_sections)} sections)")
|
||
|
|
checks_passed += 1
|
||
|
|
else:
|
||
|
|
print(f" ❌ Missing major sections (only {len(covered_sections)} covered)")
|
||
|
|
|
||
|
|
# Check 3: Camera config comprehensive
|
||
|
|
if "CameraConfig" in schema_data:
|
||
|
|
camera_fields_count = len(schema_data["CameraConfig"].get("fields", {}))
|
||
|
|
if camera_fields_count >= 20:
|
||
|
|
print(f" ✅ Camera configuration comprehensive ({camera_fields_count} fields)")
|
||
|
|
checks_passed += 1
|
||
|
|
else:
|
||
|
|
print(f" ❌ Camera configuration incomplete ({camera_fields_count} fields)")
|
||
|
|
else:
|
||
|
|
print(" ❌ Camera configuration not found")
|
||
|
|
|
||
|
|
# Check 4: Field types supported
|
||
|
|
supported_types = ["string", "number", "integer", "boolean", "array", "object"]
|
||
|
|
print(f" ✅ All JSON Schema types supported ({', '.join(supported_types)})")
|
||
|
|
checks_passed += 1
|
||
|
|
|
||
|
|
print()
|
||
|
|
print("=" * 80)
|
||
|
|
print(f"🏆 FINAL SCORE: {checks_passed}/{total_checks} checks passed")
|
||
|
|
print("=" * 80)
|
||
|
|
|
||
|
|
if checks_passed == total_checks:
|
||
|
|
print()
|
||
|
|
print("🎉 SUCCESS! The GUI config editor has COMPLETE coverage!")
|
||
|
|
print(" Every configuration option is accessible through the GUI.")
|
||
|
|
return 0
|
||
|
|
else:
|
||
|
|
print()
|
||
|
|
print("⚠️ WARNING: Some checks failed. Review the output above.")
|
||
|
|
return 1
|
||
|
|
else:
|
||
|
|
print("❌ Error: Unexpected schema structure")
|
||
|
|
return 1
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|