- Wait for the legacy shell bridge/profile before React routes render
- Expose the shell bridge and profile through root TanStack context
- Update issue routes and shell helpers to consume the shared context
- Remove the redundant issues search normalization on read
- Refresh the affected tests around shell bootstrap and routing
- tighten the shared button and select primitives to the compact modal style
- remove issues-page select overrides that no longer need to exist
- drop the issue modal button sizing overrides so shared defaults handle the density
- bring back the old symbol-based issue category icons in the React issues UI
- keep the issue detail modal fallback aligned with the shared metadata
- add a small regression check for the restored icon set
- let the issue detail modal own its selected-issue query and loading states
- keep the issues page focused on route state and modal orchestration
- preserve the loader prefetch so the modal still opens from warm cache
- split the modal shell into smaller shared components
- move default dialog styling into the shared dialog module
- simplify the issues modals to use the shared frame/header/body/footer pieces
- keep the issues route search navigation typed against the route
- Adopt Base UI for the shared form field, input, button, and toggle wrappers
- Replace the local class-name helper with clsx to keep the primitives simpler
- Keep native textarea and select controls where they still fit the existing styling pattern
- Describe the route-slice layout under webui/src
- Call out the dash-prefixed non-routing file convention
- Explain when to use unit, route, MSW, and Playwright tests
- Point readers to the current issues slice as the example to follow
- Add a shared MSW server to the Vitest setup
- Cover issue API request, success, and error scenarios
- Add msw as a dev dependency for future API-layer tests
- Move HTTP and query-option helpers out of -issues.helpers.ts.
- Keep -issues.helpers.ts focused on pure normalization and formatting helpers.
- Update issue route and modal callers to import request code from -issues.api.ts.
- Keep ky HTTPError instances intact instead of flattening them
- Use the parsed error payload when the server sends a useful message
- Fix the Issues default search type so issueId stays optional
- Add regression tests for the JSON helper behavior
- Route Issues to the React host even while the shell is still booting
- Ignore stale bootstrap work when navigation changes mid-load
- Clear artist-detail state when leaving the page so browser back can reach Library
- Add smoke coverage for the artist-detail back-navigation path
- Re-sync the active shell page on popstate
- Keep React routes like /issues on the React host after back/forward navigation
- Preserve the existing legacy page activation path for non-React routes
- Expose SoulSyncWorkflowActions from the shell bridge
- Route album download and wishlist actions to the legacy modal helpers
- Fall back to showToast for workflow notifications
- Unblock the issue modal download button by wiring the real host contract
- Restore the shell-era issue detail layout and hero ordering.
- Keep external links color-coded by service.
- Hide track details for album issues and keep the track list compact.
- Restore legacy track-list badge colors for format and bitrate.
- Match the neutral dismiss button styling from the old modal.
- Add regression coverage for the album issue modal state.
- Replace the shell convenience script with a cross-platform Python launcher.
- Keep dev.sh as a Unix compatibility wrapper.
- Let the direct backend bind with host and port overrides.
- Update the root and webui README guidance for the new launcher.
- Preserve the backend startup behavior used by the old dev flow.
- Move issue detail selection into route search so the modal is deep-linkable and back-button friendly.
- Normalize issue category and detail params before they reach the loader.
- Keep the legacy shell URL in sync for React-owned home pages.
- Preserve the legacy issues-tour hooks on the React issues page.
- Add Escape handling, focus trapping, and focus restore to the issue detail modal.
- Add route and helper coverage for the new search-state behavior.
- Introduce dev.sh as the local backend + Vite launcher
- Document the separate backend/frontend development flow
- Note that the dev Gunicorn config restarts Python on file changes
- Note that Vite hot reloads React changes in webui
- Drop unused _resolve_webui_initial_* helpers from web_server.py.
- Remove template-side initial_nav_page and initial_client_page conditionals.
- Keep Vite asset injection and runtime page activation in the client.
- Remove duplicate button base styles from the issue detail modal CSS
- Keep only the layout and state-specific variants that the shared primitives still need
- Let the shared Button and TextArea own the common control styling
- Keep Select thin and native, with options supplied as children
- Add a simple shared Button for form actions
- Use both primitives in the issues page and report modal
- Introduce reusable input, textarea, card, button, and action components
- Use them in the issue report composer as the reference implementation
- Keep the TanStack form logic at the usage site and add focused regression coverage
- Delete the static issues page renderer and detail modal helpers
- Keep the React issues route as the only implementation
- Drop the dead mobile CSS and troubleshooter hook that only targeted the removed shell
Keep React-owned pages out of the legacy page activator during initial bootstrap, and switch the visible React host before paint when the shell mounts.
That removes the refresh flash on /issues while preserving the legacy-page behavior and browser-history stability.
Verified with the router tests and the issues smoke suite.
- re-render the React shell when legacy profile bootstrap selects or refreshes a profile
- keep the initial page fallback so direct loads still activate the legacy shell chrome
- preserve the smoke coverage for direct loads and browser history
- normalize old downloads and artists page ids back to search
- keep home-page and access checks aligned with the current route ids
- let profile edit forms save modern ids while still reading old rows
- reuse the create-form page controls when rendering edit forms
- preserve existing home_page and allowed_pages IDs that the old whitelist hid
- keep mandatory pages checked so saves do not drop them
- send / through the configured profile home page
- keep the router regression test in sync with the redirect
- preserve the legacy shell fallback for non-router bootstrap
Remove the Flask route-to-page helpers and stop passing initial active-page flags into the shell template.
The web UI now renders static page and nav markup, while the client-side shell remains responsible for establishing active page state after load. This keeps the hybrid Flask + Vite asset setup intact while reducing duplicated route/page ownership logic in the backend template layer.
Also added a previously missing /stream path to the spa exclusions
- Add @tanstack/react-form to the web UI dependencies
- Move the report issue composer fields and submit validation onto TanStack Form
- Route submit and server errors through form error state while keeping React Query for mutation execution
- Extend issue route coverage for preserving custom report titles across category changes
- Mount a React-owned issue domain host and bridge report issue actions through it
- Add typed issue creation helpers, report payload types, and shared album workflow launchers
- Expand issue detail UI parity with metadata, links, track details, and admin actions
- Remove legacy static issue modal/list/detail code and update tests for the React bridge
- File-based routing with tanstack router
- Persist top-level navigation state in url, even for most legacy pages
- Striving for an intuitive and simple folder structure where
route-related code is colocated, but the amount of files is still
kept to a minimum
- Replace native fetch with `ky`
- Familiar api, but more polished
Closes#572 (rhwc).
Navidrome has no API for setting an artist image — it reads
`artist.jpg` (or `folder.jpg`) from the artist folder during
library scans. SoulSync's `update_artist_poster` for Navidrome
was a no-op, so users only ever saw album-art-derived thumbnails
as the artist photo.
- new "Write Artist Image" button on artist detail page
- POST /api/artist/<id>/write-image-to-disk derives the artist
folder from any track's resolved file_path (reuses
_resolve_library_file_path so docker mount translation +
library.music_paths probes from #558 apply), fetches the photo
from the configured metadata source priority chain, downloads
with content-type validation, writes atomically via
`<filename>.tmp + os.replace`
- when active server is Navidrome, triggers a library scan
immediately so the file is picked up
- respects existing artist.jpg (frontend prompts before
overwriting) so user-supplied photos aren't clobbered
- works for plex / jellyfin too as a fallback layer — both
servers also read artist.jpg from disk
26 tests pin the pure helpers in core/library/artist_image.py:
folder derivation (trailing sep / empty / non-string), URL
picking (missing attr / whitespace / non-string), download
(non-image content-type / 404 / timeout / empty body), atomic
write (replace / temp-cleanup-on-failure / overwrite guard /
missing folder).
- new "Audit" button on each download row in the library history
modal opens a second modal visualizing the download lifecycle as
an interactive horizontal stepper (request → source → match →
verify → process → place) with click-to-expand detail cards
- hero header with album art + track title + meta line + status
pills (source / quality / acoustid result)
- three tabs: Lifecycle / Tags / Lyrics
- Tags tab reads the audio file live via mutagen at audit-open
time via new GET /api/library/history/<id>/file-tags endpoint;
file is the single source of truth so background enrichment
writes (audiodb / lastfm / genius / replaygain / lyrics fetch)
show up too. flat key/value rows stacked vertically (label-above-
value) so long MBIDs / URLs / joined genre lists wrap cleanly.
source IDs grouped per-service into 2-col sub-card grid.
- Lyrics tab renders the full transcript with dimmed timecodes.
- post-processing step infers observable changes from source-vs-
final state (format conversion, file rename via tag template,
folder template).
- "Download History" button also added to the Downloads page batch
panel header so it's reachable outside the dashboard.
- mobile responsive: tabs + stepper scroll horizontally, modal
goes full-screen, hero stacks below 480px.
19 helper tests pin the mutagen reader: id3 (TIT2/TPE1/TALB + TXXX
+ USLT + APIC), vorbis (FLAC dict + _id/_url passthrough), file
metadata (format / bitrate / duration), defensive paths (empty /
missing file / mutagen returns None / mutagen raises), stringify
edge cases (list / tuple / int / frame-with-text / whitespace).
- legacy duck-typed builder only checked the `album_type` key; deezer
uses `record_type`, tidal uses `type` (uppercase), some flattened
musicbrainz shapes use `primary-type` — all defaulted to album, so
EPs and singles ended up filed under Album/ in user templates that
reference $albumtype
- widen lookup to album_type / record_type / type / primary-type and
route through new pure `_normalize_album_type` helper that
case-folds + validates against the canonical token set
(album / single / ep / compilation), unknown → album
- typed-converter path (spotify / deezer / itunes / discogs / mb /
hydrabase / qobuz) unchanged — those were already correct
Discord report (CAL).
- new soulseek.search_min_delay_seconds knob forces a gap between
consecutive searches; smooths the burst pattern that trips ISP
anti-abuse (Reddit report: Bell Canada cuts the WAN after rapid
peer-connection spikes) even when the existing 35/220 sliding-window
cap isn't hit
- throttle math lifted to a pure compute_search_wait_seconds helper so
the gate logic is testable independent of asyncio.sleep + the
singleton client
- new field on settings → connections → soulseek; default 0 = disabled
so existing users see no change
15 helper-boundary tests pin defaults / no-throttle, sliding-window
cap (legacy), min-delay (the new burst-smoother), max-of-both gates,
and defensive paths.
- music_source / spotify_connected / spotify_rate_limited were reading
a non-existent 'spotify' key on _status_cache and silently falling
through to the missing-value default (always 'unknown' / False).
Routed through the canonical accessors get_primary_source +
get_spotify_status now.
- added hydrabase_connected, youtube_available, hifi_instance_count,
and always_available_metadata_sources so the debug dump reflects
the full service surface
- removed a local re-import of get_spotify_status that was making
python 3.12 treat the name as function-scoped, breaking the new
lambda above it (NameError on free variable) — module-level import
already exists
11 endpoint-level tests pin music_source / spotify_* / hydrabase_* /
youtube_available / always_available_metadata_sources / hifi_instance_count
and the defensive fall-through paths when each lookup raises.
- new track_already_owned helper wraps db.check_track_exists at
the same confidence threshold the discography backfill repair job
uses (0.7) — name+artist+album, format-agnostic so blasphemy-mode
libraries (flac → mp3 + delete original) match correctly
- endpoint runs the check after the artist + content-type filters and
before add_to_wishlist, so a second discography click on the same
artist no longer re-queues every track that already downloaded
- per-album response carries a new tracks_skipped_owned counter
alongside the existing artist/content/wishlist skip categories
Discord report (Skowl).
- drop tracks where the requested artist isn't named in track.artists
(keeps features, drops compilation / appears_on contamination)
- honor watchlist.global_include_live/remixes/acoustic/instrumentals
the same way the discography backfill repair job already does
- surface per-album skip counts in the ndjson stream (artist mismatch
+ content filter) so the ui can show what was filtered
Closes#559.