Skip to content

feat: mobile web UI + network access toggle - #231

Merged
thcp merged 2 commits into
mainfrom
feat/mobile-ui
Jun 27, 2026
Merged

feat: mobile web UI + network access toggle#231
thcp merged 2 commits into
mainfrom
feat/mobile-ui

Conversation

@thcp

@thcp thcp commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

What

A phone-optimized web UI for StemDeck, plus an opt-in "Make StemDeck available on your network" setting so other devices on the LAN can reach an instance — making the app usable end-to-end from a phone.

Mobile UI (static/mobile/, vanilla JS to match the stack)

  • Library / Mixer / Extract screens wired to the real API.
    • Library — lists /api/jobs, swipe-left to delete, "Load" into the Mixer.
    • Mixer — reuses the desktop Web Audio engine (audioEngine.js, now accepting a shared gesture-unlocked AudioContext for iOS) with faders / mute / solo / seek, real analysis, and mixdown / MP4 export.
    • Extract — URL paste + file upload → POST /api/jobs, with live SSE progress.
  • Served by a user-agent check on / (phones get mobile, everyone else the DAW; ?ui=mobile / ?ui=desktop override). Shared DOM-free helpers in static/js/shared/jobs.js.
  • Ported from the design prototype kept under design/mobile/.

Network access (app/core/settings.py, app/main.py)

  • Backend always binds 0.0.0.0; a runtime gate decides whether non-host requests are served (default off, opt-in). The host machine (loopback or its own LAN IP) is always allowed, so it can never lock itself out.
  • Settings reorganized into General / Advanced tabs:
    • General — max track length (capped at 20 min) and MP4 video quality.
    • Advanced — the network toggle + the list of reachable LAN addresses, and the out-of-sync resync table.
  • Runtime settings (allow_network, max_duration_sec, video_max_height) persist to settings.json and are read live via GET/POST /api/settings — no restart.

Performance

  • Stem MP3s are transcoded once and cached on disk (previously re-encoded on every request). First load transcodes; every load after is ~instant, and the phone caches them too.

Desktop

  • start_backend binds 0.0.0.0; adds a local_ip command. Validated with cargo clippy.

Tests

  • Added tests/test_mobile_routing.py and tests/test_network_gate.py (UA routing, the host/LAN-IP gate, settings round-trip + clamps), plus tests/conftest.py isolating the gate. 127 passed, ruff + format clean.

Add a phone-optimized web UI and let other devices on the LAN reach a
StemDeck instance, so the app is usable end-to-end from a phone.

Mobile UI (static/mobile/, vanilla JS to match the stack):
- Library, Mixer, and Extract screens wired to the real API. Library lists
  /api/jobs with swipe-to-delete; Mixer reuses the desktop Web Audio engine
  (audioEngine.js, now accepting a shared gesture-unlocked AudioContext for
  iOS) with faders/mute/solo/seek, real analysis, and mixdown/MP4 export;
  Extract submits URL/upload and follows SSE progress.
- Served by a user-agent check on "/" (phones get mobile, everyone else the
  DAW; ?ui= overrides). Shared DOM-free helpers in static/js/shared/jobs.js.
- Ported from the design prototype kept under design/mobile/.

Network access (app/core/settings.py, app/main.py):
- Backend always binds 0.0.0.0; a runtime gate decides whether non-host
  requests are served (default off, opt-in). The host machine (loopback or
  its own LAN IP) is always allowed, so it can't be locked out.
- Settings dialog reorganized into General / Advanced tabs: General holds
  max track length (<=20 min) and MP4 video quality; Advanced holds the
  network toggle (with the LAN address list) and out-of-sync resync.
- Runtime settings (allow_network, max_duration_sec, video_max_height) are
  persisted and read live via GET/POST /api/settings, no restart needed.

Performance: stem MP3s are transcoded once and cached on disk (was re-encoded
on every request), so loading a track on mobile is fast and re-loads instant.

Desktop: start_backend binds 0.0.0.0; adds a local_ip command.
Comment thread app/main.py Fixed
Comment thread app/main.py Fixed
Comment thread app/core/settings.py Fixed
Comment thread app/core/settings.py Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
Comment thread design/mobile/StemDeck-Mobile.dc.html Fixed
…k design refs

- _local_ips() and settings _load()/_save(): replace bare `except: pass` with
  an explanatory comment + logging.debug/warning(exc_info=True); behavior
  unchanged (still best-effort).
- _load(): handle the no-file case explicitly (FileNotFoundError) vs. logging
  genuinely corrupt files.
- Untrack design/ (the imported Claude Design prototype) and gitignore it — it's
  a local spec reference, not shipped code, and the static analyzer's "no-effect
  expression" flags on its <x-dc> template bindings were false positives.
@thcp
thcp merged commit cde1739 into main Jun 27, 2026
8 checks passed
@thcp
thcp deleted the feat/mobile-ui branch June 27, 2026 18:04
thcp added a commit that referenced this pull request Jun 27, 2026
Follow-ups to the mobile UI (#231):

- Mixer waveform now fills yellow as playback progresses (the played bars,
  not just the playhead), and repaints on seek.
- Library/Mixer/mini-player show the real YouTube/SoundCloud thumbnail when
  available (layered over the gradient as a fallback), not just a letter.
- Configurable port (Settings -> Advanced): default 8080, persisted, read by
  the desktop launcher before spawning the backend (falls back to a free port
  if taken). A stable port means a stable phone URL. Applies on restart.
- Settings General tab: number fields are digit-only text inputs (no spinner
  arrows), length-capped; max track length capped at 20 min with the limit
  noted in the description; controls aligned. Added a Done button.

Co-authored-by: Thales <>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant