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) {
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),