soulsync/webui
Broque Thomas 8c41b05fe8 Refactor refresh_mirrored to use unified PlaylistSource registry
Phase 1a of the Discover-to-Sync unification. The mirrored-playlist
refresh handler used to branch per-source through a ~190-line
if/elif chain (Spotify, Spotify public, Deezer, Tidal, YouTube).
Each branch hand-built its own ``extra_data`` JSON for the matched-
data block. With every new source we considered for Sync-page mirror
support (ListenBrainz, Last.fm radio, SoulSync Discovery, iTunes
link), that chain would have grown a new elif.

This commit lifts the per-source logic into the existing adapter
layer and collapses the dispatch to a registry lookup:

- ``core/playlists/sources/deezer.py`` — new adapter so the registry
  covers every source the refresh handler previously branched on.
- ``core/playlists/sources/bootstrap.py`` — single helper that builds
  a populated registry from injected getter callables. Both
  ``web_server.py`` boot and the automation test fixtures call it,
  so the two construction paths can't drift.
- ``core/playlists/sources/base.py`` — ``to_mirror_track_dict``
  projection helper centralises the NormalizedTrack → DB-row
  conversion (including the discovered/matched_data and
  spotify_hint extra_data shapes the downstream sync + wishlist
  consumers already expect).
- Spotify adapter now populates ``extra['discovered']`` + an
  ``extra['matched_data']`` block when fetching via the authed API,
  so Spotify mirrors keep landing pre-discovered (matches the
  pre-refactor contract pinned by
  ``test_spotify_refresh_writes_to_db``).
- Spotify-public adapter populates ``extra['spotify_hint']`` so the
  discovery worker can skip its search step and jump straight to
  enrichment for the known track ID.
- All artist-name fields now project to first-artist-only across
  every adapter — matches the pre-refactor mirror_playlist DB shape
  (``t.artists[0]``).

``refresh_mirrored.py`` shrinks ~190 → ~80 lines and keeps:

- the file/beatport unrefreshable-source filter,
- URL extraction from ``description`` via ``require_refresh_url``
  for spotify_public + youtube,
- the Spotify-public → authed-Spotify fallback when the user is
  signed in (handler-level branch, not in any adapter),
- the Tidal-not-authenticated soft-skip log (skip, not error),
- existing-extra_data preservation across refreshes,
- the ``playlist_changed`` automation event emit on track-set delta.

Test scaffolding:

- ``_build_deps`` in ``tests/automation/test_handlers_playlist.py``
  now builds a default registry from the passed clients via
  ``build_playlist_source_registry``, so existing refresh tests
  exercise the same path without per-test changes. New tests cover
  Tidal-not-authed soft-skip, Deezer refresh writes plain tracks,
  YouTube refresh reads URL from description, and Spotify-public
  uses authed Spotify when signed in.
- 4 new adapter tests for Deezer projection +
  ``to_mirror_track_dict`` (minimal track, Spotify matched_data,
  Spotify-public spotify_hint).
- ``playlist_source_registry`` field on ``AutomationDeps`` defaults
  to ``None`` so the other 5 automation test files (which don't
  exercise refresh_mirrored) keep working unchanged.

220 tests across automation + adapter suites green.
2026-05-26 12:52:39 -07:00
..
docs/migration docs(import): align migration docs 2026-05-24 21:17:22 +03:00
src Fix Zustand shallow import 2026-05-24 14:47:29 -07:00
static Refactor refresh_mirrored to use unified PlaylistSource registry 2026-05-26 12:52:39 -07:00
tests fix(webui): preserve import tab refresh URLs 2026-05-24 21:17:20 +03:00
.gitignore Initial Vite app scaffolding & issues page impl 2026-05-13 22:24:46 +03:00
.oxfmtrc.json Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
.oxlintrc.json Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
index.html Sync page: dedicated iTunes Link icon + reorder Qobuz tab 2026-05-26 11:32:38 -07:00
package-lock.json feat(webui): migrate import route to React 2026-05-24 21:11:40 +03:00
package.json feat(webui): migrate import route to React 2026-05-24 21:11:40 +03:00
playwright.config.ts Initial Vite app scaffolding & issues page impl 2026-05-13 22:24:46 +03:00
README.md docs(webui): group migration planning docs 2026-05-23 21:22:44 +03:00
tsconfig.json Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
vite.config.ts Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
vitest.config.ts Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
vitest.setup.ts feat(webui): expose shell status in root context 2026-05-23 21:23:32 +03:00

WebUI Hybrid Rendering

SoulSync's web UI is in a transition phase:

  • most pages still render through the legacy vanilla JS shell
  • /issues is rendered by the new React app
  • a small shell bridge keeps both runtimes aware of the active page, profile context, and navigation state

How It Fits Together

flowchart LR
    Browser["Browser parses /webui/index.html"]
    Legacy["Legacy shell scripts\n(core.js -> ... -> init.js)"]
    Bridge["shell-bridge.js\nwindow.SoulSyncWebShellBridge"]
    React["Vite React app\nsrc/app/main.tsx"]
    Router["TanStack Router\nwindow.SoulSyncWebRouter"]

    Browser --> Legacy
    Browser --> React
    Legacy --> Bridge
    React --> Router
    Router --> Bridge
    Bridge --> Legacy

Runtime Roles

  • webui/static/init.js

    • boots the legacy shell
    • selects the active profile
    • handles the legacy page loading flow
  • webui/static/shell-bridge.js

    • owns the browser-side bridge object
    • exposes window.SoulSyncWebShellBridge
    • owns the shared page chrome and route handoff helpers
  • webui/src/app/main.tsx

    • mounts the React app
    • binds window.SoulSyncWebRouter
  • webui/src/platform/shell/route-controllers.tsx

    • listens for bridge readiness
    • keeps React pages aligned with the shell

Load Order

The current order in index.html matters:

  1. legacy shell scripts load first
  2. init.js sets up the shell runtime
  3. shell-bridge.js publishes the bridge and shared chrome helpers after the shell state exists
  4. the Vite React app is injected through {{ vite_assets('body') }} and boots as a module after parsing

That order avoids load-time references to missing globals and keeps the React side able to react to bridge readiness events. The React entry can start fetching early, but the shell bridge and legacy globals are already available by the time the React runtime starts acting on them.

Notes

  • The bridge is intentionally small and browser-only.
  • This is the start of the migration, not a full replacement of the legacy shell.
  • When adding another React page, check whether it needs:
    • a route entry in webui/src/platform/shell/route-manifest.ts
    • bridge typings in webui/src/platform/shell/globals.d.ts
    • a legacy fallback path in webui/static/init.js
    • bridge glue or handoff logic in webui/static/shell-bridge.js

Folder Layout

The React webui uses a small set of predictable folders so route slices stay easy to extend, test, and understand.

webui/src/
  app/         React bootstrap, router, query client, shared API client
  components/  Shared UI primitives
  platform/    Shell bridge and browser/platform integration
  routes/      Route-local code and TanStack Router pages
  test/        Shared test utilities and setup helpers

Migration planning docs live under webui/docs/migration/.

  • keep the high-level route backlog there
  • add one route-specific sketch per migration task
  • keep migration notes close to the WebUI code rather than the repo root

Route Slices

  • Keep route-specific code inside webui/src/routes/<route>/.
  • Put the routing entry in route.tsx.
  • Put route-local UI in a -ui/ folder.
  • Prefix non-routing files with - so TanStack Router ignores them.
  • Keep the route slice small and cohesive.
  • Prefer a few files with clear responsibilities over many tiny files with overlapping names.

Example:

webui/src/routes/issues/
  route.tsx
  -issues.types.ts
  -issues.api.ts
  -issues.helpers.ts
  -issues.api.test.ts
  -issues.helpers.test.ts
  -ui/
    issues-page.tsx
    issue-detail-modal.tsx
    issue-domain-host.tsx

The initial issues slice is the model to follow:

  • -issues.api.ts holds request code and query options
  • -issues.helpers.ts holds pure normalization and formatting
  • -issues.types.ts holds shared types
  • -ui/ holds the page, modal, and legacy handoff UI

Shared Code

  • Put reusable UI in webui/src/components/.
  • Put shell integration in webui/src/platform/.
  • Put bootstrap and app-wide wiring in webui/src/app/.
  • Move code up a level only when it is genuinely shared.
  • Avoid creating new conventions that overlap with existing ones.

Testing Choices

We have a lot of testing tools available, but we do not need all of them for every feature.

  • Use plain unit tests for pure functions and small transforms.
  • Use React component or route tests when the behavior lives in the UI or router.
  • Use MSW-backed tests when request shape, response handling, or error handling matters.
  • Use Playwright when the behavior is best proven end-to-end with the server and browser together.
  • Prefer the smallest test setup that still proves the thing that can regress.

Development

The repo root now owns the full local-dev instructions. Start there for the portable launcher and backend/frontend setup:

  1. README.md for the end-to-end dev flow
  2. npm run check and npm run fix for React-side linting and formatting