diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py index 7cf88ca..6b92913 100644 --- a/server/src/wled_controller/main.py +++ b/server/src/wled_controller/main.py @@ -57,11 +57,6 @@ value_source_store = ValueSourceStore(config.storage.value_sources_file) automation_store = AutomationStore(config.storage.automations_file) scene_preset_store = ScenePresetStore(config.storage.scene_presets_file) -# Migrate embedded audio config from CSS entities to audio sources -audio_source_store.migrate_from_css(color_strip_store) -# Assign default audio template to multichannel sources that have none -audio_source_store.migrate_add_default_template(audio_template_store) - processor_manager = ProcessorManager( picture_source_store=picture_source_store, capture_template_store=template_store, diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index 558497f..fe1e686 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -93,7 +93,7 @@ import { showAddDevice, closeAddDeviceModal, scanForDevices, handleAddDevice, } from './features/device-discovery.js'; import { - loadTargetsTab, loadTargets, switchTargetSubTab, + loadTargetsTab, switchTargetSubTab, showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor, startTargetProcessing, stopTargetProcessing, startTargetOverlay, stopTargetOverlay, deleteTarget, @@ -330,7 +330,6 @@ Object.assign(window, { // targets loadTargetsTab, - loadTargets, switchTargetSubTab, expandAllTargetSections, collapseAllTargetSections, showTargetEditor, diff --git a/server/src/wled_controller/static/js/features/device-discovery.js b/server/src/wled_controller/static/js/features/device-discovery.js index 8d4dce7..f76e54d 100644 --- a/server/src/wled_controller/static/js/features/device-discovery.js +++ b/server/src/wled_controller/static/js/features/device-discovery.js @@ -337,8 +337,7 @@ export async function handleAddDevice(event) { let url; if (isMockDevice(deviceType)) { - const ledCount = document.getElementById('device-led-count')?.value || '60'; - url = `mock://${ledCount}`; + url = 'mock://'; } else if (isSerialDevice(deviceType)) { url = document.getElementById('device-serial-port').value; } else { diff --git a/server/src/wled_controller/static/js/features/devices.js b/server/src/wled_controller/static/js/features/devices.js index a223d1e..eca93ac 100644 --- a/server/src/wled_controller/static/js/features/devices.js +++ b/server/src/wled_controller/static/js/features/devices.js @@ -31,8 +31,8 @@ class DeviceSettingsModal extends Modal { _getUrl() { if (isMockDevice(this.deviceType)) { - const ledCount = this.$('settings-led-count')?.value || '60'; - return `mock://${ledCount}`; + const deviceId = this.$('settings-device-id')?.value || ''; + return `mock://${deviceId}`; } if (isSerialDevice(this.deviceType)) { return this.$('settings-serial-port').value; @@ -83,7 +83,7 @@ export function createDeviceCard(device) {
${device.name || device.id} - ${device.url && device.url.startsWith('http') ? `${escapeHtml(device.url.replace(/^https?:\/\//, ''))}${ICON_WEB}` : (device.url && !device.url.startsWith('http') ? `${escapeHtml(device.url)}` : '')} + ${device.url && device.url.startsWith('http') ? `${escapeHtml(device.url.replace(/^https?:\/\//, ''))}${ICON_WEB}` : (device.url && !device.url.startsWith('mock://') && !device.url.startsWith('http') ? `${escapeHtml(device.url)}` : '')} ${healthLabel}
diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index 7ca178f..7a70609 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -438,11 +438,6 @@ export async function saveTargetEditor() { // ===== TARGETS TAB (WLED devices + targets combined) ===== -export async function loadTargets() { - // Alias for backward compatibility - await loadTargetsTab(); -} - export function switchTargetSubTab(tabKey) { document.querySelectorAll('.target-sub-tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.targetSubTab === tabKey) @@ -572,9 +567,7 @@ export async function loadTargetsTab() { const runningCount = targetsWithState.filter(t => t.state && t.state.processing).length; updateTabBadge('targets', runningCount); - // Backward compat: map stored "wled" sub-tab to "led" - let activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led'; - if (activeSubTab === 'wled') activeSubTab = 'led'; + const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led'; const subTabs = [ { key: 'led', icon: getTargetTypeIcon('led'), titleKey: 'targets.subtab.led', count: ledDevices.length + Object.keys(colorStripSourceMap).length + ledTargets.length }, diff --git a/server/src/wled_controller/static/js/features/tutorials.js b/server/src/wled_controller/static/js/features/tutorials.js index 09f7287..587ba09 100644 --- a/server/src/wled_controller/static/js/features/tutorials.js +++ b/server/src/wled_controller/static/js/features/tutorials.js @@ -249,6 +249,12 @@ function showTutorialStep(index, direction = 1) { target.classList.add('tutorial-target'); if (isFixed) target.style.zIndex = '10001'; + // Scroll target into view if off-screen + const preRect = target.getBoundingClientRect(); + if (preRect.bottom > window.innerHeight || preRect.top < 0) { + target.scrollIntoView({ behavior: 'instant', block: 'center' }); + } + const targetRect = target.getBoundingClientRect(); const pad = 6; let x, y, w, h; diff --git a/server/src/wled_controller/storage/audio_source_store.py b/server/src/wled_controller/storage/audio_source_store.py index a97ce13..d20f974 100644 --- a/server/src/wled_controller/storage/audio_source_store.py +++ b/server/src/wled_controller/storage/audio_source_store.py @@ -232,116 +232,3 @@ class AudioSourceStore: raise ValueError(f"Audio source {source_id} is not a valid audio source") - def resolve_mono_source(self, mono_id: str) -> Tuple[int, bool, str, Optional[str]]: - """Backward-compatible wrapper for resolve_audio_source().""" - return self.resolve_audio_source(mono_id) - - # ── Migration ──────────────────────────────────────────────────── - - def migrate_from_css(self, color_strip_store) -> None: - """One-time migration: extract audio config from existing CSS entities. - - For each AudioColorStripSource that has old-style embedded audio fields - (audio_device_index, audio_loopback, audio_channel) but no audio_source_id: - 1. Create a MultichannelAudioSource if one with matching config doesn't exist - 2. Create a MonoAudioSource referencing it - 3. Set audio_source_id on the CSS entity - 4. Save both stores - """ - from wled_controller.storage.color_strip_source import AudioColorStripSource - - migrated = 0 - multichannel_cache: Dict[Tuple[int, bool], str] = {} # (dev, loopback) → id - - # Index existing multichannel sources for dedup - for source in self._sources.values(): - if isinstance(source, MultichannelAudioSource): - key = (source.device_index, source.is_loopback) - multichannel_cache[key] = source.id - - for css in color_strip_store.get_all_sources(): - if not isinstance(css, AudioColorStripSource): - continue - # Skip if already migrated - if getattr(css, "audio_source_id", None): - continue - # Skip if no old fields present - if not hasattr(css, "audio_device_index"): - continue - - dev_idx = getattr(css, "audio_device_index", -1) - loopback = bool(getattr(css, "audio_loopback", True)) - channel = getattr(css, "audio_channel", "mono") or "mono" - - # Find or create multichannel source - mc_key = (dev_idx, loopback) - if mc_key in multichannel_cache: - mc_id = multichannel_cache[mc_key] - else: - device_label = "Loopback" if loopback else "Input" - mc_name = f"Audio Device {dev_idx} ({device_label})" - # Ensure unique name - suffix = 2 - base_name = mc_name - while any(s.name == mc_name for s in self._sources.values()): - mc_name = f"{base_name} #{suffix}" - suffix += 1 - - mc_id = f"as_{uuid.uuid4().hex[:8]}" - now = datetime.utcnow() - mc_source = MultichannelAudioSource( - id=mc_id, name=mc_name, source_type="multichannel", - created_at=now, updated_at=now, - device_index=dev_idx, is_loopback=loopback, - ) - self._sources[mc_id] = mc_source - multichannel_cache[mc_key] = mc_id - logger.info(f"Migration: created multichannel source '{mc_name}' ({mc_id})") - - # Create mono source - channel_label = {"mono": "Mono", "left": "Left", "right": "Right"}.get(channel, channel) - mono_name = f"{css.name} - {channel_label}" - # Ensure unique name - suffix = 2 - base_name = mono_name - while any(s.name == mono_name for s in self._sources.values()): - mono_name = f"{base_name} #{suffix}" - suffix += 1 - - mono_id = f"as_{uuid.uuid4().hex[:8]}" - now = datetime.utcnow() - mono_source = MonoAudioSource( - id=mono_id, name=mono_name, source_type="mono", - created_at=now, updated_at=now, - audio_source_id=mc_id, channel=channel, - ) - self._sources[mono_id] = mono_source - logger.info(f"Migration: created mono source '{mono_name}' ({mono_id})") - - # Update CSS entity - css.audio_source_id = mono_id - migrated += 1 - - if migrated > 0: - self._save() - color_strip_store._save() - logger.info(f"Migration complete: migrated {migrated} audio CSS entities") - else: - logger.debug("No audio CSS entities needed migration") - - def migrate_add_default_template(self, audio_template_store) -> None: - """Assign the default audio template to multichannel sources that have no template.""" - default_id = audio_template_store.get_default_template_id() - if not default_id: - logger.debug("No default audio template — skipping template migration") - return - - migrated = 0 - for source in self._sources.values(): - if isinstance(source, MultichannelAudioSource) and not source.audio_template_id: - source.audio_template_id = default_id - migrated += 1 - - if migrated > 0: - self._save() - logger.info(f"Assigned default audio template to {migrated} multichannel sources") diff --git a/server/src/wled_controller/storage/automation_store.py b/server/src/wled_controller/storage/automation_store.py index a3f73a4..3783e0c 100644 --- a/server/src/wled_controller/storage/automation_store.py +++ b/server/src/wled_controller/storage/automation_store.py @@ -28,8 +28,7 @@ class AutomationStore: with open(self.file_path, "r", encoding="utf-8") as f: data = json.load(f) - # Support both old "profiles" key and new "automations" key - automations_data = data.get("automations", data.get("profiles", {})) + automations_data = data.get("automations", {}) loaded = 0 for auto_id, auto_dict in automations_data.items(): try: diff --git a/server/src/wled_controller/storage/device_store.py b/server/src/wled_controller/storage/device_store.py index 0f96c90..9013a2c 100644 --- a/server/src/wled_controller/storage/device_store.py +++ b/server/src/wled_controller/storage/device_store.py @@ -48,8 +48,6 @@ class Device: self.rgbw = rgbw self.created_at = created_at or datetime.utcnow() self.updated_at = updated_at or datetime.utcnow() - # Preserved from old JSON for migration — not written back - self._legacy_calibration = None def to_dict(self) -> dict: """Convert device to dictionary.""" @@ -77,12 +75,8 @@ class Device: @classmethod def from_dict(cls, data: dict) -> "Device": - """Create device from dictionary. - - Backward-compatible: reads legacy 'calibration' field and stores it - in _legacy_calibration for migration use only. - """ - device = cls( + """Create device from dictionary.""" + return cls( device_id=data["id"], name=data["name"], url=data["url"], @@ -98,17 +92,6 @@ class Device: updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())), ) - # Preserve old calibration for migration (never written back by to_dict) - calibration_data = data.get("calibration") - if calibration_data: - try: - from wled_controller.core.capture.calibration import calibration_from_dict - device._legacy_calibration = calibration_from_dict(calibration_data) - except Exception: - pass - - return device - class DeviceStore: """Persistent storage for WLED devices.""" @@ -196,6 +179,10 @@ class DeviceStore: """Create a new device.""" device_id = f"device_{uuid.uuid4().hex[:8]}" + # Mock devices use their device ID as the URL authority + if device_type == "mock": + url = f"mock://{device_id}" + device = Device( device_id=device_id, name=name, diff --git a/server/src/wled_controller/storage/picture_target.py b/server/src/wled_controller/storage/picture_target.py index 57cd066..b129c67 100644 --- a/server/src/wled_controller/storage/picture_target.py +++ b/server/src/wled_controller/storage/picture_target.py @@ -56,9 +56,8 @@ class PictureTarget: @classmethod def from_dict(cls, data: dict) -> "PictureTarget": """Create from dictionary, dispatching to the correct subclass.""" - target_type = data.get("target_type", "wled") - # "wled" and "led" both map to WledPictureTarget (backward compat) - if target_type in ("wled", "led"): + target_type = data.get("target_type", "led") + if target_type == "led": from wled_controller.storage.wled_picture_target import WledPictureTarget return WledPictureTarget.from_dict(data) if target_type == "key_colors": diff --git a/server/src/wled_controller/storage/scene_preset.py b/server/src/wled_controller/storage/scene_preset.py index 9ba213c..09ae121 100644 --- a/server/src/wled_controller/storage/scene_preset.py +++ b/server/src/wled_controller/storage/scene_preset.py @@ -75,7 +75,7 @@ class AutomationSnapshot: @classmethod def from_dict(cls, data: dict) -> "AutomationSnapshot": return cls( - automation_id=data.get("automation_id", data.get("profile_id", "")), + automation_id=data.get("automation_id", ""), enabled=data.get("enabled", True), ) @@ -118,7 +118,7 @@ class ScenePreset: color=data.get("color", "#4fc3f7"), targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])], devices=[DeviceBrightnessSnapshot.from_dict(d) for d in data.get("devices", [])], - automations=[AutomationSnapshot.from_dict(a) for a in data.get("automations", data.get("profiles", []))], + automations=[AutomationSnapshot.from_dict(a) for a in data.get("automations", [])], order=data.get("order", 0), created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())), updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())), diff --git a/server/src/wled_controller/storage/wled_picture_target.py b/server/src/wled_controller/storage/wled_picture_target.py index 45719fb..61e9152 100644 --- a/server/src/wled_controller/storage/wled_picture_target.py +++ b/server/src/wled_controller/storage/wled_picture_target.py @@ -105,26 +105,16 @@ class WledPictureTarget(PictureTarget): @classmethod def from_dict(cls, data: dict) -> "WledPictureTarget": - """Create from dictionary with backward compatibility.""" - # New format: direct color_strip_source_id - if "color_strip_source_id" in data: - css_id = data["color_strip_source_id"] - # Old format: segments array — take first segment's css_id - elif "segments" in data: - segs = data["segments"] - css_id = segs[0].get("color_strip_source_id", "") if segs else "" - else: - css_id = "" - + """Create from dictionary.""" return cls( id=data["id"], name=data["name"], target_type="led", device_id=data.get("device_id", ""), - color_strip_source_id=css_id, + color_strip_source_id=data.get("color_strip_source_id", ""), brightness_value_source_id=data.get("brightness_value_source_id", ""), fps=data.get("fps", 30), - keepalive_interval=data.get("keepalive_interval", data.get("standby_interval", 1.0)), + keepalive_interval=data.get("keepalive_interval", 1.0), state_check_interval=data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL), min_brightness_threshold=data.get("min_brightness_threshold", 0), adaptive_fps=data.get("adaptive_fps", False),