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:
@@ -1,7 +1,7 @@
|
||||
"""Adalight serial LED client — sends pixel data over serial using the Adalight protocol."""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
@@ -199,7 +199,7 @@ class AdalightClient(LEDClient):
|
||||
return DeviceHealth(
|
||||
online=True,
|
||||
latency_ms=0.0,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
device_name=prev_health.device_name if prev_health else None,
|
||||
device_version=None,
|
||||
device_led_count=prev_health.device_led_count if prev_health else None,
|
||||
@@ -207,12 +207,12 @@ class AdalightClient(LEDClient):
|
||||
else:
|
||||
return DeviceHealth(
|
||||
online=False,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
error=f"Serial port {port} not found",
|
||||
)
|
||||
except Exception as e:
|
||||
return DeviceHealth(
|
||||
online=False,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
@@ -190,9 +190,12 @@ class DDPClient:
|
||||
try:
|
||||
# Send plain RGB — WLED handles per-bus color order conversion
|
||||
# internally when outputting to hardware.
|
||||
# Convert to numpy to avoid per-pixel Python loop
|
||||
# Accept numpy arrays directly to avoid per-pixel Python loop
|
||||
bpp = 4 if self.rgbw else 3 # bytes per pixel
|
||||
pixel_array = np.array(pixels, dtype=np.uint8)
|
||||
if isinstance(pixels, np.ndarray):
|
||||
pixel_array = pixels
|
||||
else:
|
||||
pixel_array = np.array(pixels, dtype=np.uint8)
|
||||
if self.rgbw:
|
||||
n = pixel_array.shape[0]
|
||||
if n != self._rgbw_buf_n:
|
||||
@@ -219,7 +222,7 @@ class DDPClient:
|
||||
for i in range(num_packets):
|
||||
start = i * bytes_per_packet
|
||||
end = min(start + bytes_per_packet, total_bytes)
|
||||
chunk = bytes(pixel_bytes[start:end])
|
||||
chunk = pixel_bytes[start:end]
|
||||
is_last = (i == num_packets - 1)
|
||||
|
||||
# Increment sequence number
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
@@ -139,7 +139,7 @@ class LEDClient(ABC):
|
||||
http_client: Shared httpx.AsyncClient for HTTP requests
|
||||
prev_health: Previous health result (for preserving cached metadata)
|
||||
"""
|
||||
return DeviceHealth(online=True, last_checked=datetime.utcnow())
|
||||
return DeviceHealth(online=True, last_checked=datetime.now(timezone.utc))
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.connect()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Mock LED client — simulates an LED strip with configurable latency for testing."""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
@@ -69,5 +69,5 @@ class MockClient(LEDClient):
|
||||
return DeviceHealth(
|
||||
online=True,
|
||||
latency_ms=0.0,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Mock device provider — virtual LED strip for testing."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from wled_controller.core.devices.led_client import (
|
||||
@@ -28,7 +28,7 @@ class MockDeviceProvider(LEDDeviceProvider):
|
||||
return MockClient(url, **kwargs)
|
||||
|
||||
async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
|
||||
return DeviceHealth(online=True, latency_ms=0.0, last_checked=datetime.utcnow())
|
||||
return DeviceHealth(online=True, latency_ms=0.0, last_checked=datetime.now(timezone.utc))
|
||||
|
||||
async def validate_device(self, url: str) -> dict:
|
||||
return {}
|
||||
|
||||
@@ -87,12 +87,12 @@ class MQTTLEDClient(LEDClient):
|
||||
http_client,
|
||||
prev_health=None,
|
||||
) -> DeviceHealth:
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
svc = _mqtt_service
|
||||
if svc is None or not svc.is_enabled:
|
||||
return DeviceHealth(online=False, error="MQTT disabled", last_checked=datetime.utcnow())
|
||||
return DeviceHealth(online=False, error="MQTT disabled", last_checked=datetime.now(timezone.utc))
|
||||
return DeviceHealth(
|
||||
online=svc.is_connected,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
error=None if svc.is_connected else "MQTT broker disconnected",
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
@@ -428,13 +428,13 @@ class OpenRGBLEDClient(LEDClient):
|
||||
return DeviceHealth(
|
||||
online=True,
|
||||
latency_ms=latency,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
device_name=device_name,
|
||||
device_led_count=device_led_count,
|
||||
)
|
||||
except Exception as e:
|
||||
return DeviceHealth(
|
||||
online=False,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Tuple, Optional, Dict, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -540,7 +540,7 @@ class WLEDClient(LEDClient):
|
||||
return DeviceHealth(
|
||||
online=True,
|
||||
latency_ms=round(latency, 1),
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
device_name=data.get("name"),
|
||||
device_version=data.get("ver"),
|
||||
device_led_count=leds_info.get("count"),
|
||||
@@ -553,7 +553,7 @@ class WLEDClient(LEDClient):
|
||||
return DeviceHealth(
|
||||
online=False,
|
||||
latency_ms=None,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
device_name=prev_health.device_name if prev_health else None,
|
||||
device_version=prev_health.device_version if prev_health else None,
|
||||
device_led_count=prev_health.device_led_count if prev_health else None,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""WebSocket LED client — broadcasts pixel data to connected WebSocket clients."""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
@@ -126,5 +126,5 @@ class WSLEDClient(LEDClient):
|
||||
return DeviceHealth(
|
||||
online=True,
|
||||
latency_ms=0.0,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_checked=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""WebSocket device provider — factory, validation, health checks."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from wled_controller.core.devices.led_client import (
|
||||
@@ -33,7 +33,7 @@ class WSDeviceProvider(LEDDeviceProvider):
|
||||
self, url: str, http_client, prev_health=None,
|
||||
) -> DeviceHealth:
|
||||
return DeviceHealth(
|
||||
online=True, latency_ms=0.0, last_checked=datetime.utcnow(),
|
||||
online=True, latency_ms=0.0, last_checked=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
async def validate_device(self, url: str) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user