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

@@ -52,6 +52,7 @@ def _device_to_response(device) -> DeviceResponse:
rgbw=device.rgbw,
zone_mode=device.zone_mode,
capabilities=sorted(get_device_capabilities(device.device_type)),
tags=getattr(device, 'tags', []),
created_at=device.created_at,
updated_at=device.updated_at,
)
@@ -126,6 +127,7 @@ async def create_device(
send_latency_ms=device_data.send_latency_ms or 0,
rgbw=device_data.rgbw or False,
zone_mode=device_data.zone_mode or "combined",
tags=device_data.tags,
)
# WS devices: auto-set URL to ws://{device_id}
@@ -308,6 +310,7 @@ async def update_device(
send_latency_ms=update_data.send_latency_ms,
rgbw=update_data.rgbw,
zone_mode=update_data.zone_mode,
tags=update_data.tags,
)
# Sync connection info in processor manager
@@ -322,11 +325,12 @@ async def update_device(
pass
# Sync auto_shutdown and zone_mode in runtime state
if device_id in manager._devices:
ds = manager.find_device_state(device_id)
if ds:
if update_data.auto_shutdown is not None:
manager._devices[device_id].auto_shutdown = update_data.auto_shutdown
ds.auto_shutdown = update_data.auto_shutdown
if update_data.zone_mode is not None:
manager._devices[device_id].zone_mode = update_data.zone_mode
ds.zone_mode = update_data.zone_mode
return _device_to_response(device)
@@ -420,7 +424,7 @@ async def get_device_brightness(
raise HTTPException(status_code=400, detail=f"Brightness control is not supported for {device.device_type} devices")
# Return cached hardware brightness if available (updated by SET endpoint)
ds = manager._devices.get(device_id)
ds = manager.find_device_state(device_id)
if ds and ds.hardware_brightness is not None:
return {"brightness": ds.hardware_brightness}
@@ -465,13 +469,15 @@ async def set_device_brightness(
except NotImplementedError:
# Provider has no hardware brightness; use software brightness
device.software_brightness = bri
device.updated_at = __import__("datetime").datetime.utcnow()
from datetime import datetime, timezone
device.updated_at = datetime.now(timezone.utc)
store.save()
if device_id in manager._devices:
manager._devices[device_id].software_brightness = bri
ds = manager.find_device_state(device_id)
if ds:
ds.software_brightness = bri
# Update cached hardware brightness
ds = manager._devices.get(device_id)
ds = manager.find_device_state(device_id)
if ds:
ds.hardware_brightness = bri
@@ -499,7 +505,7 @@ async def get_device_power(
try:
# Serial devices: use tracked state (no hardware query available)
ds = manager._devices.get(device_id)
ds = manager.find_device_state(device_id)
if device.device_type in ("adalight", "ambiled") and ds:
return {"on": ds.power_on}
@@ -532,10 +538,10 @@ async def set_device_power(
try:
# For serial devices, use the cached idle client to avoid port conflicts
ds = manager._devices.get(device_id)
ds = manager.find_device_state(device_id)
if device.device_type in ("adalight", "ambiled") and ds:
if not on:
await manager._send_clear_pixels(device_id)
await manager.send_clear_pixels(device_id)
ds.power_on = on
else:
provider = get_provider(device.device_type)