Add capture template system with in-memory defaults and split device settings UI
Some checks failed
Validate / validate (push) Failing after 8s

- Generate default templates (MSS, DXcam, WGC) in memory from EngineRegistry at startup
- Only persist user-created templates to JSON, skip defaults on load/save
- Add capture_template_id to Device model and DeviceCreate schema
- Remember last used template in localStorage, use it for new devices with fallback
- Split Device Settings dialog into General Settings and Capture Settings
- Add capture settings button (🎬) to device card
- Separate default and custom templates with visual separator in Templates tab
- Add capture engine integration to ProcessorManager
- Add CLAUDE.md with git commit/push policy and server restart instructions
- Add en/ru localization for all new UI elements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 02:43:49 +03:00
parent b5545d3198
commit 5370d80466
15 changed files with 772 additions and 106 deletions

View File

@@ -30,6 +30,7 @@ class Device:
enabled: bool = True,
settings: Optional[ProcessingSettings] = None,
calibration: Optional[CalibrationConfig] = None,
capture_template_id: str = "tpl_mss_default",
created_at: Optional[datetime] = None,
updated_at: Optional[datetime] = None,
):
@@ -43,6 +44,7 @@ class Device:
enabled: Whether device is enabled
settings: Processing settings
calibration: Calibration configuration
capture_template_id: ID of assigned capture template
created_at: Creation timestamp
updated_at: Last update timestamp
"""
@@ -53,6 +55,7 @@ class Device:
self.enabled = enabled
self.settings = settings or ProcessingSettings()
self.calibration = calibration or create_default_calibration(led_count)
self.capture_template_id = capture_template_id
self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow()
@@ -80,6 +83,7 @@ class Device:
"state_check_interval": self.settings.state_check_interval,
},
"calibration": calibration_to_dict(self.calibration),
"capture_template_id": self.capture_template_id,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}
@@ -117,6 +121,12 @@ class Device:
else create_default_calibration(data["led_count"])
)
# Migration: assign default MSS template if no template set
capture_template_id = data.get("capture_template_id")
if not capture_template_id:
capture_template_id = "tpl_mss_default"
logger.info(f"Migrating device {data['id']} to default MSS template")
return cls(
device_id=data["id"],
name=data["name"],
@@ -125,6 +135,7 @@ class Device:
enabled=data.get("enabled", True),
settings=settings,
calibration=calibration,
capture_template_id=capture_template_id,
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
)
@@ -206,6 +217,7 @@ class DeviceStore:
led_count: int,
settings: Optional[ProcessingSettings] = None,
calibration: Optional[CalibrationConfig] = None,
capture_template_id: str = "tpl_mss_default",
) -> Device:
"""Create a new device.
@@ -215,6 +227,7 @@ class DeviceStore:
led_count: Number of LEDs
settings: Processing settings
calibration: Calibration configuration
capture_template_id: ID of assigned capture template
Returns:
Created device
@@ -233,6 +246,7 @@ class DeviceStore:
led_count=led_count,
settings=settings,
calibration=calibration,
capture_template_id=capture_template_id,
)
# Store
@@ -270,6 +284,7 @@ class DeviceStore:
enabled: Optional[bool] = None,
settings: Optional[ProcessingSettings] = None,
calibration: Optional[CalibrationConfig] = None,
capture_template_id: Optional[str] = None,
) -> Device:
"""Update device.
@@ -281,6 +296,7 @@ class DeviceStore:
enabled: New enabled state (optional)
settings: New settings (optional)
calibration: New calibration (optional)
capture_template_id: New capture template ID (optional)
Returns:
Updated device
@@ -313,6 +329,8 @@ class DeviceStore:
f"does not match device LED count ({device.led_count})"
)
device.calibration = calibration
if capture_template_id is not None:
device.capture_template_id = capture_template_id
device.updated_at = datetime.utcnow()

View File

@@ -6,6 +6,7 @@ from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from wled_controller.core.capture_engines.factory import EngineRegistry
from wled_controller.storage.template import CaptureTemplate
from wled_controller.utils import get_logger
@@ -13,7 +14,11 @@ logger = get_logger(__name__)
class TemplateStore:
"""Storage for capture templates."""
"""Storage for capture templates.
Default templates for each available engine are created in memory at startup.
Only user-created templates are persisted to the JSON file.
"""
def __init__(self, file_path: str):
"""Initialize template store.
@@ -23,13 +28,35 @@ class TemplateStore:
"""
self.file_path = Path(file_path)
self._templates: Dict[str, CaptureTemplate] = {}
self._ensure_defaults()
self._load()
def _ensure_defaults(self) -> None:
"""Create default templates in memory for all available engines."""
available = EngineRegistry.get_available_engines()
now = datetime.utcnow()
for engine_type in available:
template_id = f"tpl_{engine_type}_default"
engine_class = EngineRegistry.get_engine(engine_type)
default_config = engine_class.get_default_config()
self._templates[template_id] = CaptureTemplate(
id=template_id,
name=engine_type.upper(),
engine_type=engine_type,
engine_config=default_config,
is_default=True,
created_at=now,
updated_at=now,
description=f"Default {engine_type} capture template",
)
logger.info(f"Created {len(available)} default templates in memory")
def _load(self) -> None:
"""Load templates from file."""
"""Load user-created templates from file."""
if not self.file_path.exists():
logger.warning(f"Templates file not found: {self.file_path}")
self._save() # Create empty file
return
try:
@@ -37,33 +64,42 @@ class TemplateStore:
data = json.load(f)
templates_data = data.get("templates", {})
loaded = 0
for template_id, template_dict in templates_data.items():
# Skip any default templates that may exist in old files
if template_dict.get("is_default", False):
continue
try:
template = CaptureTemplate.from_dict(template_dict)
self._templates[template_id] = template
loaded += 1
except Exception as e:
logger.error(
f"Failed to load template {template_id}: {e}",
exc_info=True
)
logger.info(f"Loaded {len(self._templates)} templates from storage")
logger.info(f"Template store initialized with {len(self._templates)} templates")
if loaded > 0:
logger.info(f"Loaded {loaded} user templates from storage")
except Exception as e:
logger.error(f"Failed to load templates from {self.file_path}: {e}")
raise
total = len(self._templates)
logger.info(f"Template store initialized with {total} templates")
def _save(self) -> None:
"""Save templates to file."""
"""Save only user-created templates to file."""
try:
# Ensure directory exists
self.file_path.parent.mkdir(parents=True, exist_ok=True)
# Convert templates to dict format
# Only persist non-default templates
templates_dict = {
template_id: template.to_dict()
for template_id, template in self._templates.items()
if not template.is_default
}
data = {