diff --git a/server/src/wled_controller/api/dependencies.py b/server/src/wled_controller/api/dependencies.py index de6f4c9..f3ee254 100644 --- a/server/src/wled_controller/api/dependencies.py +++ b/server/src/wled_controller/api/dependencies.py @@ -157,6 +157,23 @@ def get_sync_clock_manager() -> SyncClockManager: return _sync_clock_manager +def fire_entity_event(entity_type: str, action: str, entity_id: str) -> None: + """Fire an entity_changed event via the ProcessorManager event bus. + + Args: + entity_type: e.g. "device", "output_target", "color_strip_source" + action: "created", "updated", or "deleted" + entity_id: The entity's unique ID + """ + if _processor_manager is not None: + _processor_manager.fire_event({ + "type": "entity_changed", + "entity_type": entity_type, + "action": action, + "id": entity_id, + }) + + def init_dependencies( device_store: DeviceStore, template_store: TemplateStore, diff --git a/server/src/wled_controller/api/routes/audio_sources.py b/server/src/wled_controller/api/routes/audio_sources.py index bad4bf7..7c1b197 100644 --- a/server/src/wled_controller/api/routes/audio_sources.py +++ b/server/src/wled_controller/api/routes/audio_sources.py @@ -9,6 +9,7 @@ from starlette.websockets import WebSocket, WebSocketDisconnect from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_audio_source_store, get_audio_template_store, get_color_strip_store, @@ -84,6 +85,7 @@ async def create_audio_source( audio_template_id=data.audio_template_id, tags=data.tags, ) + fire_entity_event("audio_source", "created", source.id) return _to_response(source) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -123,6 +125,7 @@ async def update_audio_source( audio_template_id=data.audio_template_id, tags=data.tags, ) + fire_entity_event("audio_source", "updated", source_id) return _to_response(source) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -146,6 +149,7 @@ async def delete_audio_source( ) store.delete_source(source_id) + fire_entity_event("audio_source", "deleted", source_id) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) diff --git a/server/src/wled_controller/api/routes/audio_templates.py b/server/src/wled_controller/api/routes/audio_templates.py index aba8bc0..157e68a 100644 --- a/server/src/wled_controller/api/routes/audio_templates.py +++ b/server/src/wled_controller/api/routes/audio_templates.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query from starlette.websockets import WebSocket, WebSocketDisconnect from wled_controller.api.auth import AuthRequired -from wled_controller.api.dependencies import get_audio_template_store, get_audio_source_store, get_processor_manager +from wled_controller.api.dependencies import fire_entity_event, get_audio_template_store, get_audio_source_store, get_processor_manager from wled_controller.api.schemas.audio_templates import ( AudioEngineInfo, AudioEngineListResponse, @@ -66,6 +66,7 @@ async def create_audio_template( engine_config=data.engine_config, description=data.description, tags=data.tags, ) + fire_entity_event("audio_template", "created", template.id) return AudioTemplateResponse( id=template.id, name=template.name, engine_type=template.engine_type, engine_config=template.engine_config, tags=getattr(template, 'tags', []), @@ -112,6 +113,7 @@ async def update_audio_template( engine_type=data.engine_type, engine_config=data.engine_config, description=data.description, tags=data.tags, ) + fire_entity_event("audio_template", "updated", template_id) return AudioTemplateResponse( id=t.id, name=t.name, engine_type=t.engine_type, engine_config=t.engine_config, tags=getattr(t, 'tags', []), @@ -135,6 +137,7 @@ async def delete_audio_template( """Delete an audio template.""" try: store.delete_template(template_id, audio_source_store=audio_source_store) + fire_entity_event("audio_template", "deleted", template_id) except HTTPException: raise except ValueError as e: diff --git a/server/src/wled_controller/api/routes/automations.py b/server/src/wled_controller/api/routes/automations.py index 0fd2e11..0b8652c 100644 --- a/server/src/wled_controller/api/routes/automations.py +++ b/server/src/wled_controller/api/routes/automations.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_automation_engine, get_automation_store, get_scene_preset_store, @@ -174,6 +175,7 @@ async def create_automation( if automation.enabled: await engine.trigger_evaluate() + fire_entity_event("automation", "created", automation.id) return _automation_to_response(automation, engine, request) @@ -273,6 +275,7 @@ async def update_automation( if automation.enabled: await engine.trigger_evaluate() + fire_entity_event("automation", "updated", automation_id) return _automation_to_response(automation, engine, request) @@ -296,6 +299,8 @@ async def delete_automation( except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) + fire_entity_event("automation", "deleted", automation_id) + # ===== Enable/Disable ===== diff --git a/server/src/wled_controller/api/routes/devices.py b/server/src/wled_controller/api/routes/devices.py index 214aa87..f943ceb 100644 --- a/server/src/wled_controller/api/routes/devices.py +++ b/server/src/wled_controller/api/routes/devices.py @@ -12,6 +12,7 @@ from wled_controller.core.devices.led_client import ( get_provider, ) from wled_controller.api.dependencies import ( + fire_entity_event, get_device_store, get_output_target_store, get_processor_manager, @@ -146,6 +147,7 @@ async def create_device( zone_mode=device.zone_mode, ) + fire_entity_event("device", "created", device.id) return _device_to_response(device) except HTTPException: @@ -332,6 +334,7 @@ async def update_device( if update_data.zone_mode is not None: ds.zone_mode = update_data.zone_mode + fire_entity_event("device", "updated", device_id) return _device_to_response(device) except ValueError as e: @@ -369,6 +372,7 @@ async def delete_device( # Delete from storage store.delete_device(device_id) + fire_entity_event("device", "deleted", device_id) logger.info(f"Deleted device {device_id}") except HTTPException: diff --git a/server/src/wled_controller/api/routes/output_targets.py b/server/src/wled_controller/api/routes/output_targets.py index cb4b2aa..058fcdf 100644 --- a/server/src/wled_controller/api/routes/output_targets.py +++ b/server/src/wled_controller/api/routes/output_targets.py @@ -12,6 +12,7 @@ from PIL import Image from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_color_strip_store, get_device_store, get_pattern_template_store, @@ -181,6 +182,7 @@ async def create_target( except ValueError as e: logger.warning(f"Could not register target {target.id} in processor manager: {e}") + fire_entity_event("output_target", "created", target.id) return _target_to_response(target) except HTTPException: @@ -319,6 +321,7 @@ async def update_target( except ValueError: pass + fire_entity_event("output_target", "updated", target_id) return _target_to_response(target) except HTTPException: @@ -354,6 +357,7 @@ async def delete_target( # Delete from store target_store.delete_target(target_id) + fire_entity_event("output_target", "deleted", target_id) logger.info(f"Deleted target {target_id}") except ValueError as e: diff --git a/server/src/wled_controller/api/routes/pattern_templates.py b/server/src/wled_controller/api/routes/pattern_templates.py index 3d46b7c..325f3bb 100644 --- a/server/src/wled_controller/api/routes/pattern_templates.py +++ b/server/src/wled_controller/api/routes/pattern_templates.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, HTTPException, Depends from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_pattern_template_store, get_output_target_store, ) @@ -73,6 +74,7 @@ async def create_pattern_template( description=data.description, tags=data.tags, ) + fire_entity_event("pattern_template", "created", template.id) return _pat_template_to_response(template) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -117,6 +119,7 @@ async def update_pattern_template( description=data.description, tags=data.tags, ) + fire_entity_event("pattern_template", "updated", template_id) return _pat_template_to_response(template) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -143,6 +146,7 @@ async def delete_pattern_template( "Please reassign those targets before deleting.", ) store.delete_template(template_id) + fire_entity_event("pattern_template", "deleted", template_id) except HTTPException: raise except ValueError as e: diff --git a/server/src/wled_controller/api/routes/picture_sources.py b/server/src/wled_controller/api/routes/picture_sources.py index 03fa619..dbe5946 100644 --- a/server/src/wled_controller/api/routes/picture_sources.py +++ b/server/src/wled_controller/api/routes/picture_sources.py @@ -12,6 +12,7 @@ from fastapi.responses import Response from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_picture_source_store, get_output_target_store, get_pp_template_store, @@ -199,6 +200,7 @@ async def create_picture_source( description=data.description, tags=data.tags, ) + fire_entity_event("picture_source", "created", stream.id) return _stream_to_response(stream) except HTTPException: raise @@ -244,6 +246,7 @@ async def update_picture_source( description=data.description, tags=data.tags, ) + fire_entity_event("picture_source", "updated", stream_id) return _stream_to_response(stream) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -271,6 +274,7 @@ async def delete_picture_source( "Please reassign those targets before deleting.", ) store.delete_stream(stream_id) + fire_entity_event("picture_source", "deleted", stream_id) except HTTPException: raise except ValueError as e: diff --git a/server/src/wled_controller/api/routes/postprocessing.py b/server/src/wled_controller/api/routes/postprocessing.py index 9fbc36c..3a8695a 100644 --- a/server/src/wled_controller/api/routes/postprocessing.py +++ b/server/src/wled_controller/api/routes/postprocessing.py @@ -11,6 +11,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSock from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_picture_source_store, get_pp_template_store, get_template_store, @@ -84,6 +85,7 @@ async def create_pp_template( description=data.description, tags=data.tags, ) + fire_entity_event("pp_template", "created", template.id) return _pp_template_to_response(template) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -123,6 +125,7 @@ async def update_pp_template( description=data.description, tags=data.tags, ) + fire_entity_event("pp_template", "updated", template_id) return _pp_template_to_response(template) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -150,6 +153,7 @@ async def delete_pp_template( "Please reassign those streams before deleting.", ) store.delete_template(template_id) + fire_entity_event("pp_template", "deleted", template_id) except HTTPException: raise except ValueError as e: diff --git a/server/src/wled_controller/api/routes/scene_presets.py b/server/src/wled_controller/api/routes/scene_presets.py index 9764e78..2bf3ee9 100644 --- a/server/src/wled_controller/api/routes/scene_presets.py +++ b/server/src/wled_controller/api/routes/scene_presets.py @@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_output_target_store, get_processor_manager, get_scene_preset_store, @@ -87,6 +88,7 @@ async def create_scene_preset( except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) + fire_entity_event("scene_preset", "created", preset.id) return _preset_to_response(preset) @@ -175,6 +177,7 @@ async def update_scene_preset( ) except ValueError as e: raise HTTPException(status_code=404 if "not found" in str(e).lower() else 400, detail=str(e)) + fire_entity_event("scene_preset", "updated", preset_id) return _preset_to_response(preset) @@ -194,6 +197,7 @@ async def delete_scene_preset( except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) + fire_entity_event("scene_preset", "deleted", preset_id) # ===== Recapture ===== @@ -259,4 +263,5 @@ async def activate_scene_preset( if not errors: logger.info(f"Scene preset '{preset.name}' activated successfully") + fire_entity_event("scene_preset", "updated", preset_id) return ActivateResponse(status=status, errors=errors) diff --git a/server/src/wled_controller/api/routes/sync_clocks.py b/server/src/wled_controller/api/routes/sync_clocks.py index d7ed877..1fb2aa3 100644 --- a/server/src/wled_controller/api/routes/sync_clocks.py +++ b/server/src/wled_controller/api/routes/sync_clocks.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_color_strip_store, get_sync_clock_manager, get_sync_clock_store, @@ -70,6 +71,7 @@ async def create_sync_clock( description=data.description, tags=data.tags, ) + fire_entity_event("sync_clock", "created", clock.id) return _to_response(clock, manager) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -110,6 +112,7 @@ async def update_sync_clock( # Hot-update runtime speed if data.speed is not None: manager.update_speed(clock_id, clock.speed) + fire_entity_event("sync_clock", "updated", clock_id) return _to_response(clock, manager) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -133,6 +136,7 @@ async def delete_sync_clock( ) manager.release_all_for(clock_id) store.delete_clock(clock_id) + fire_entity_event("sync_clock", "deleted", clock_id) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -152,6 +156,7 @@ async def pause_sync_clock( except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) manager.pause(clock_id) + fire_entity_event("sync_clock", "updated", clock_id) return _to_response(clock, manager) @@ -168,6 +173,7 @@ async def resume_sync_clock( except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) manager.resume(clock_id) + fire_entity_event("sync_clock", "updated", clock_id) return _to_response(clock, manager) @@ -184,4 +190,5 @@ async def reset_sync_clock( except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) manager.reset(clock_id) + fire_entity_event("sync_clock", "updated", clock_id) return _to_response(clock, manager) diff --git a/server/src/wled_controller/api/routes/templates.py b/server/src/wled_controller/api/routes/templates.py index 4fbdc76..e308fda 100644 --- a/server/src/wled_controller/api/routes/templates.py +++ b/server/src/wled_controller/api/routes/templates.py @@ -10,6 +10,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSock from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_picture_source_store, get_pp_template_store, get_template_store, @@ -96,6 +97,7 @@ async def create_template( tags=template_data.tags, ) + fire_entity_event("capture_template", "created", template.id) return TemplateResponse( id=template.id, name=template.name, @@ -156,6 +158,7 @@ async def update_template( tags=update_data.tags, ) + fire_entity_event("capture_template", "updated", template_id) return TemplateResponse( id=template.id, name=template.name, @@ -202,6 +205,7 @@ async def delete_template( # Proceed with deletion template_store.delete_template(template_id) + fire_entity_event("capture_template", "deleted", template_id) except HTTPException: raise # Re-raise HTTP exceptions as-is diff --git a/server/src/wled_controller/api/routes/value_sources.py b/server/src/wled_controller/api/routes/value_sources.py index 69a0f62..3bfeb25 100644 --- a/server/src/wled_controller/api/routes/value_sources.py +++ b/server/src/wled_controller/api/routes/value_sources.py @@ -8,6 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSock from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + fire_entity_event, get_output_target_store, get_processor_manager, get_value_source_store, @@ -100,6 +101,7 @@ async def create_value_source( auto_gain=data.auto_gain, tags=data.tags, ) + fire_entity_event("value_source", "created", source.id) return _to_response(source) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -150,6 +152,7 @@ async def update_value_source( ) # Hot-reload running value streams pm.update_value_source(source_id) + fire_entity_event("value_source", "updated", source_id) return _to_response(source) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -174,6 +177,7 @@ async def delete_value_source( ) store.delete_source(source_id) + fire_entity_event("value_source", "deleted", source_id) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) diff --git a/server/src/wled_controller/core/automations/automation_engine.py b/server/src/wled_controller/core/automations/automation_engine.py index 09fa3c4..b11819f 100644 --- a/server/src/wled_controller/core/automations/automation_engine.py +++ b/server/src/wled_controller/core/automations/automation_engine.py @@ -393,7 +393,7 @@ class AutomationEngine: def _fire_event(self, automation_id: str, action: str) -> None: try: - self._manager._fire_event({ + self._manager.fire_event({ "type": "automation_state_changed", "automation_id": automation_id, "action": action, diff --git a/server/src/wled_controller/core/processing/processor_manager.py b/server/src/wled_controller/core/processing/processor_manager.py index 062db99..c57b5e6 100644 --- a/server/src/wled_controller/core/processing/processor_manager.py +++ b/server/src/wled_controller/core/processing/processor_manager.py @@ -158,7 +158,7 @@ class ProcessorManager: device_store=self._device_store, color_strip_stream_manager=self._color_strip_stream_manager, value_stream_manager=self._value_stream_manager, - fire_event=self._fire_event, + fire_event=self.fire_event, get_device_info=self._get_device_info, ) @@ -203,8 +203,8 @@ class ProcessorManager: if queue in self._event_queues: self._event_queues.remove(queue) - def _fire_event(self, event: dict) -> None: - """Push event to all subscribers (non-blocking).""" + def fire_event(self, event: dict) -> None: + """Push event to all subscribers (non-blocking). Public API for route handlers.""" for q in self._event_queues: try: q.put_nowait(event) @@ -854,7 +854,7 @@ class ProcessorManager: f"[AUTO-RESTART] Target {target_id} crashed {rs.attempts} times " f"in {now - rs.first_crash_time:.0f}s — giving up" ) - self._fire_event({ + self.fire_event({ "type": "state_change", "target_id": target_id, "processing": False, @@ -872,7 +872,7 @@ class ProcessorManager: f"{_RESTART_MAX_ATTEMPTS}), restarting in {backoff:.1f}s" ) - self._fire_event({ + self.fire_event({ "type": "state_change", "target_id": target_id, "processing": False, @@ -916,7 +916,7 @@ class ProcessorManager: await self.start_processing(target_id) except Exception as e: logger.error(f"[AUTO-RESTART] Failed to restart {target_id}: {e}") - self._fire_event({ + self.fire_event({ "type": "state_change", "target_id": target_id, "processing": False, @@ -1050,11 +1050,21 @@ class ProcessorManager: state = self._devices.get(device_id) if not state: return + prev_online = state.health.online client = await self._get_http_client() state.health = await check_device_health( state.device_type, state.device_url, client, state.health, ) + # Fire event when online status changes + if state.health.online != prev_online: + self.fire_event({ + "type": "device_health_changed", + "device_id": device_id, + "online": state.health.online, + "latency_ms": state.health.latency_ms, + }) + # Auto-sync LED count reported = state.health.device_led_count if reported and reported != state.led_count and self._device_store: diff --git a/server/src/wled_controller/static/js/core/entity-events.js b/server/src/wled_controller/static/js/core/entity-events.js new file mode 100644 index 0000000..9b6b99a --- /dev/null +++ b/server/src/wled_controller/static/js/core/entity-events.js @@ -0,0 +1,61 @@ +/** + * Entity event listeners — reacts to server-pushed entity_changed and + * device_health_changed WebSocket events by invalidating the relevant + * DataCache and dispatching an `entity:reload` DOM event so active + * feature modules can refresh their UI. + */ + +import { + devicesCache, outputTargetsCache, colorStripSourcesCache, + streamsCache, audioSourcesCache, valueSourcesCache, + syncClocksCache, automationsCacheObj, scenePresetsCache, + captureTemplatesCache, audioTemplatesCache, ppTemplatesCache, + patternTemplatesCache, +} from './state.js'; + +/** Maps entity_type string from the server event to its DataCache instance. */ +const ENTITY_CACHE_MAP = { + device: devicesCache, + output_target: outputTargetsCache, + color_strip_source: colorStripSourcesCache, + picture_source: streamsCache, + audio_source: audioSourcesCache, + value_source: valueSourcesCache, + sync_clock: syncClocksCache, + automation: automationsCacheObj, + scene_preset: scenePresetsCache, + capture_template: captureTemplatesCache, + audio_template: audioTemplatesCache, + pp_template: ppTemplatesCache, + pattern_template: patternTemplatesCache, +}; + +function _invalidateAndReload(entityType) { + const cache = ENTITY_CACHE_MAP[entityType]; + if (cache) { + cache.invalidate(); + cache.fetch(); + } + document.dispatchEvent(new CustomEvent('entity:reload', { + detail: { entity_type: entityType }, + })); +} + +function _onEntityChanged(e) { + const { entity_type } = e.detail || {}; + if (!entity_type) return; + _invalidateAndReload(entity_type); +} + +function _onDeviceHealthChanged() { + _invalidateAndReload('device'); +} + +/** + * Register listeners for server-pushed entity events. + * Call once during app initialization, after startEventsWS(). + */ +export function startEntityEventListeners() { + document.addEventListener('server:entity_changed', _onEntityChanged); + document.addEventListener('server:device_health_changed', _onDeviceHealthChanged); +} diff --git a/server/src/wled_controller/static/js/core/events-ws.js b/server/src/wled_controller/static/js/core/events-ws.js index 3c0dcaf..833ef0f 100644 --- a/server/src/wled_controller/static/js/core/events-ws.js +++ b/server/src/wled_controller/static/js/core/events-ws.js @@ -2,13 +2,20 @@ * Global events WebSocket — stays connected while logged in, * dispatches DOM custom events that feature modules can listen to. * - * Events dispatched: server:state_change, server:automation_state_changed + * Events dispatched: + * server:state_change — target processing start/stop/crash + * server:automation_state_changed — automation activated/deactivated + * server:entity_changed — entity CRUD (create/update/delete) + * server:device_health_changed — device online/offline status change */ import { apiKey } from './state.js'; let _ws = null; let _reconnectTimer = null; +let _reconnectDelay = 1000; // start at 1s, exponential backoff to 30s +const _RECONNECT_MIN = 1000; +const _RECONNECT_MAX = 30000; export function startEventsWS() { stopEventsWS(); @@ -19,6 +26,9 @@ export function startEventsWS() { try { _ws = new WebSocket(url); + _ws.onopen = () => { + _reconnectDelay = _RECONNECT_MIN; // reset backoff on successful connection + }; _ws.onmessage = (event) => { try { const data = JSON.parse(event.data); @@ -27,7 +37,8 @@ export function startEventsWS() { }; _ws.onclose = () => { _ws = null; - _reconnectTimer = setTimeout(startEventsWS, 3000); + _reconnectTimer = setTimeout(startEventsWS, _reconnectDelay); + _reconnectDelay = Math.min(_reconnectDelay * 2, _RECONNECT_MAX); }; _ws.onerror = () => {}; } catch { @@ -45,4 +56,5 @@ export function stopEventsWS() { _ws.close(); _ws = null; } + _reconnectDelay = _RECONNECT_MIN; } diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index ecce09f..7373cb7 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -840,6 +840,13 @@ function _debouncedDashboardReload(forceFullRender = false) { document.addEventListener('server:state_change', () => _debouncedDashboardReload()); document.addEventListener('server:automation_state_changed', () => _debouncedDashboardReload(true)); +document.addEventListener('server:device_health_changed', () => _debouncedDashboardReload()); + +const _DASHBOARD_ENTITY_TYPES = new Set(['output_target', 'automation', 'scene_preset', 'sync_clock', 'device']); +document.addEventListener('server:entity_changed', (e) => { + const { entity_type } = e.detail || {}; + if (_DASHBOARD_ENTITY_TYPES.has(entity_type)) _debouncedDashboardReload(true); +}); // Re-render dashboard when language changes document.addEventListener('languageChanged', () => {