P5-R3: Web app frontend — touch-optimized mixer UI
Lint & Validate / lint (push) Has been cancelled

- Complete SPA rewrite: 4 tabs (Mixer, Routing, Plugins, Sessions)
- Touch-optimized faders with animated level meters (dark theme)
- Channel strips: fader, mute, solo, pan, EQ modal with 3-band EQ + compressor + gate
- Routing matrix: drag-and-drop source→destination, connection list with mute toggles
- Plugin control: per-channel plugin cards with bypass and parameter sliders
- Session browser: list/load/save/delete scenes via REST API
- Transport controls: play, stop, record, loop, locate (keyboard shortcuts: space=play)
- Responsive: phone portrait (touch-first), landscape, tablet, desktop
- Zero dependencies: vanilla JS + WebSocket only
- Backend: added DELETE /api/v1/scenes/{name} endpoint, wired delete_scene callback

web/app.js (917 lines), web/style.css (1100 lines), web/index.html (160 lines)
This commit is contained in:
2026-05-19 21:05:16 -04:00
parent 9cd8292acc
commit b79d7288c5
9 changed files with 2006 additions and 418 deletions
+20 -8
View File
@@ -107,8 +107,8 @@ class BackingTrackPlayer:
self._playback_position: float = 0.0 # seconds into active track
self._count_in_remaining: float = 0.0 # seconds of count-in left
# Thread safety
self._lock = threading.Lock()
# Thread safety (reentrant — play() may be called from next_track() etc.)
self._lock = threading.RLock()
# DSP integration
self._dsp_engine = self.config.dsp_engine
@@ -232,15 +232,20 @@ class BackingTrackPlayer:
track = pl.current_track
if track is None:
logger.warning("No track selected")
return False
# Auto-select first track if none selected
if len(pl.tracks) > 0:
pl.current_index = 0
track = pl.tracks[0]
else:
logger.warning("No track selected")
return False
# Load if needed
if not track.loaded and track.file_path:
if not self.load_track(track.id):
return False
if track_id not in self._audio_cache:
if track.id not in self._audio_cache:
logger.warning("Track audio not loaded: %s", track.id)
return False
@@ -373,8 +378,9 @@ class BackingTrackPlayer:
logger.info("End of playlist reached")
return False
self._active_track_id = track.id
if was_playing:
self.play(track.id)
self.play(track.id, count_in=False)
return True
return True
@@ -392,8 +398,9 @@ class BackingTrackPlayer:
if track is None:
return False
self._active_track_id = track.id
if was_playing:
self.play(track.id)
self.play(track.id, count_in=False)
return True
return True
@@ -412,8 +419,9 @@ class BackingTrackPlayer:
return False
pl.current_index = index
self._active_track_id = track.id
if was_playing:
self.play(track.id)
self.play(track.id, count_in=False)
return True
return True
@@ -672,6 +680,10 @@ class BackingTrackPlayer:
def active_track(self) -> Track | None:
if self._active_track_id:
return self.playlists.get_track(self._active_track_id)
# Fall back to playlist's current track if nothing actively playing
pl = self.playlists.active
if pl is not None:
return pl.current_track
return None
@property