Add tags to all entity types with chip-based input and autocomplete

- Add `tags: List[str]` field to all 13 entity types (devices, output targets,
  CSS sources, picture sources, audio sources, value sources, sync clocks,
  automations, scene presets, capture/audio/PP/pattern templates)
- Update all stores, schemas, and route handlers for tag CRUD
- Add GET /api/v1/tags endpoint aggregating unique tags across all stores
- Create TagInput component with chip display, autocomplete dropdown,
  keyboard navigation, and API-backed suggestions
- Display tag chips on all entity cards (searchable via existing text filter)
- Add tag input to all 14 editor modals with dirty check support
- Add CSS styles and i18n keys (en/ru/zh) for tag UI
- Also includes code review fixes: thread safety, perf, store dedup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 22:20:19 +03:00
parent 2712c6682e
commit 30fa107ef7
120 changed files with 2471 additions and 1949 deletions

View File

@@ -1,8 +1,8 @@
"""LED output target — sends color strip sources to an LED device."""
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
from datetime import datetime, timezone
from typing import List, Optional
from wled_controller.storage.output_target import OutputTarget
@@ -63,9 +63,10 @@ class WledOutputTarget(OutputTarget):
brightness_value_source_id=None,
fps=None, keepalive_interval=None, state_check_interval=None,
min_brightness_threshold=None, adaptive_fps=None, protocol=None,
description=None, **_kwargs) -> None:
description=None, tags: Optional[List[str]] = None,
**_kwargs) -> None:
"""Apply mutable field updates for WLED targets."""
super().update_fields(name=name, description=description)
super().update_fields(name=name, description=description, tags=tags)
if device_id is not None:
self.device_id = device_id
if color_strip_source_id is not None:
@@ -120,6 +121,7 @@ class WledOutputTarget(OutputTarget):
adaptive_fps=data.get("adaptive_fps", False),
protocol=data.get("protocol", "ddp"),
description=data.get("description"),
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
tags=data.get("tags", []),
created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.now(timezone.utc).isoformat())),
)