Commit graph

2005 commits

Author SHA1 Message Date
BoulderBadgeDad
91912681e5 fix: Deezer source dot red — probe GET-ed a POST-only test endpoint
Some checks failed
Dev Nightly Build / nightly (push) Has been cancelled
The "Test all sources" Deezer probe called _ssJson("/api/deezer-download/test") with no opts, i.e. a
GET — but that route is POST-only, so it 405s, the probe throws, and the dot goes red even though Deezer
downloads fine. POST it with an empty body (the endpoint then tests the SAVED ARL). The other probe
endpoints (qobuz/hifi/amazon/soundcloud status) are all GET, so this was the only offender.
2026-06-29 17:12:42 -07:00
BoulderBadgeDad
fcd768ffee discovery: tune the wave dial — green->blue spectrum, calmer waves, subtler orb, refined header
Feedback pass: red read as "bad", so the spectrum is now green -> blue through cyan (deep/exploratory,
not alarming). The orb glow was way too hot — cut it right down and softened the pulse ring + the colour
aura. Dialed the wiggle back (lower amplitude, fewer cycles, slower) so the adventurous end is lively,
not chaotic. Header is now a small uppercase "ADVENTUROUSNESS" overline above the big colour-shifting
state word, instead of the plain bold title.
2026-06-29 16:54:53 -07:00
BoulderBadgeDad
d9bd5c1d7f discovery: elevate the wave dial — full width + glow, luminous fill, pulsing orb, colour aura 2026-06-29 16:48:34 -07:00
BoulderBadgeDad
c50465984f discovery: replace the slider with a living wave dial (animated)
Scrapped the basic slider for a custom animated control matching the vision: a draggable orb rides an
SVG line that is redrawn every frame (rAF). At the left (value 0) the line is green and waves gently —
"artists you already like", alive but calm; drag the orb right and the colour shifts through the warm
spectrum (green -> yellow -> orange -> red) while the wave grows taller and more erratic (a detuned
second harmonic). The orb glows the current colour and rides the wave; the state word recolours too.
Releasing saves (POST /api/discover/adventurousness) and re-fetches both rec rows. The loop skips work
while the page is hidden. Still one source of truth with the Settings slider.

64 script-integrity tests green.
2026-06-29 16:41:12 -07:00
BoulderBadgeDad
43fbab5704 discovery: rebuild the Adventurousness dial — visible track + compact layout
The first cut had two problems (screenshot): the gradient track was invisible (Chromium does not render
the range inputs own background as the track when appearance:none — must style ::-webkit-slider-runnable-
track / ::-moz-range-track) and the grid layout stretched it full-width into a sparse void.

Now: a contained (max-width 880) single-row flex control — icon | title+state | Safe | gradient track |
Adventurous | live %. The gradient (green->accent->orange) renders on the track pseudo-elements, the
thumb is centred on it, and a % readout anchors the right edge. Mobile wraps the slider to its own row.
2026-06-29 16:33:45 -07:00
BoulderBadgeDad
71a48dd344 discovery: fancy Adventurousness dial on the Discover page (synced with Settings)
A prominent control above the rec rows: gradient slider (Safe green -> accent -> hot orange), glowing
grab-thumb, a compass icon that swaps with the level (lifebuoy/compass/dice/crystal-ball) and a live
state word ("Playing it safe" -> "Deep cuts only"). Dragging updates the label live; releasing saves
and immediately re-fetches both rec rows so the effect is instant.

Shares the config key discover.adventurousness with the Settings -> Discovery slider via a new
GET/POST /api/discover/adventurousness endpoint, so the two controls stay in sync (change one, the
other reflects it on next load) — one source of truth, no divergence.

64 script-integrity tests green; route ruff-clean + compiles.
2026-06-29 16:26:50 -07:00
BoulderBadgeDad
29f84b5601 discovery: add Settings -> Discovery -> Adventurousness slider + align config key
A 0..1 range slider (Safe <-> Adventurous) in Settings -> Discovery with a live value readout. It
auto-saves via the settings page existing range-input listener, persists to config under
discover.adventurousness (matching the /api/settings "discover" section), and re-populates on load.
The listening-recs route reads that key (fixed from discovery. -> discover. to match the settings
section). Drag it up and "Based On Your Listening" leans harder toward obscure picks on the next load.

64 script-integrity tests green.
2026-06-29 16:13:51 -07:00
BoulderBadgeDad
052547c04a discover: reorder sections into Spotify-style grouping + blank optional track fields
_reorderDiscoverSections() runs after all loaders and moves each section (incl. ones nested in the
mid-page #discover-bylt-sections) into the intended top-to-bottom order: Genre Explorer -> Your/Year
Mixes -> for-you (listening/recommended/BYLT) -> new (recent/genres/seasonal) -> library (artists/
albums) -> deeper digs (undiscovered/labels/deep cuts) -> stations & tools (Last.fm/ListenBrainz/
Build). Hero + Artist Map stay near the top; hidden collapsed-mix sections are left invisible.

Also: ListenBrainz/Last.fm recording playlists carry no album or duration, so _normalizeTrack now
leaves album blank (not "Unknown Album") and renderCompactPlaylist blanks the duration when 0 (not
"0:00"), matching the original displayListenBrainzTracks behaviour.

64 script-integrity tests green.
2026-06-29 15:51:43 -07:00
BoulderBadgeDad
a236974e43 discover: fix decade/LB tracks showing Unknown — normalizer must fall back to the track itself
_normalizeTrack fell back to {} when there was no track_data_json, so it never read the top-level
name/artists[]/album that decade (and Spotify-shaped) rows carry — every field defaulted to Unknown.
Fall back to the track object itself (|| track), matching _renderTabbedTrackLists trackData = track
behaviour. Flat (track_name) + nested (track_data_json) shapes still resolve.
2026-06-29 15:39:09 -07:00
BoulderBadgeDad
391fc9ae78 discover: fix undefined tracks + give Year/Last.fm/ListenBrainz cards real mosaic covers
Two bugs in the new playlist-card sections:
- "undefined" track rows: those sources nest fields under track.track_data_json (name/artists[]/
  album/duration_ms), but renderCompactPlaylist read flat track.track_name/etc. Added _normalizeTrack
  (handles both shapes) and routed renderCompactPlaylist + the mix-card mosaic through it, so decade /
  Last.fm / ListenBrainz tracks render real titles instead of undefined.
- amateur covers: cards showed a single flat emoji. Now _hydrateMixCovers background-fetches each
  cards tracks, upgrades the placeholder to a real 2x2 album-art mosaic (matching Your Mixes), and
  caches the tracks on the mix so the modal opens instantly. Wired into _renderMixGrid (decades) and
  the ListenBrainz/Last.fm render. Dropped the emoji coverHtml.

64 script-integrity tests green.
2026-06-29 15:26:42 -07:00
BoulderBadgeDad
ec4439da00 discover: Last.fm Radio + ListenBrainz -> mix cards; remove Browse-by-Genre
buildListenBrainzPlaylistsHtml (shared by both sections) now emits a grid of mix cards instead of
full-width track-table subsections — each playlist is a card that opens its tracks (lazy-loaded from
/api/discover/listenbrainz/playlist/{id}) + Download/Sync in the shared modal. ListenBrainz sync uses
its own -sync-total/-sync-matched status spans, so the mix carries a matching statusHtml; the sync
button id (discover-lb-playlist-{id}-sync-btn) lines up so startListenBrainzPlaylistSync drives it.
The old eager loadTracksForPlaylists calls are now harmless no-ops (their -playlist containers are
gone, so loadListenBrainzPlaylistTracks early-returns). Wing-It is omitted from the card modal for now
(its dropdown needs modal-aware positioning).

Browse by Genre removed (section markup + the loadGenreBrowserTabs call): it was empty
(/api/discover/genres/available returns nothing) and redundant with the Genre Explorer pills.

64 script-integrity tests green.
2026-06-29 15:17:07 -07:00
BoulderBadgeDad
153fad152c discover: generalize the mix modal + convert Time Machine to "Year Mixes" cards
Foundation for folding the remaining playlist sections into the mix-card system:
- openMixModal now takes a custom `actions` list (not just the built-in Download/Sync), a `statusBase`
  so any sections live sync lands in the modal, a `pollerKey` for in-flight re-show, and `fetchTracks`
  for lazy sections that load tracks on open.
- _buildMixCard supports a non-mosaic `coverHtml` + `trackCount` (for art-less cards like decades).
- new _renderMixGrid(container, mixes) renders cards into any sections own grid (not the shared shelf).

Time Machine -> "Year Mixes": a shelf of decade cards (gradient cover + "1980s" label); opening one
lazy-loads /api/discover/decade/{year} into the modal (populating decadeTracksCache so Sync works)
with Download/Sync. Tabs removed.

Fix: _upsertMixCard rendered the WHOLE shared registry into Your Mixes, so decades (added by
_renderMixGrid for the modal lookup) leaked in. Your Mixes now tracks its own _yourMixKeys and renders
only those. 64 script-integrity tests green.
2026-06-29 15:09:26 -07:00
BoulderBadgeDad
880ad74a0a discover: convert the last stragglers (bylt + genre tiles) to .ya-card
Because You Listen To rows now render .ya-card grids (with the Show-all clamp) instead of the old
discover-card carousels. The genre-browser tiles (_renderGenreCard) are converted too for consistency
— that loader is currently dead code (loadGenreBrowser is never called; only the tabbed Browse-by-Genre
modal runs), so this just future-proofs it. With this, no discover-card markup remains in discover.js
— the whole Discover page is one card language (.ya-card + the mix cards), leaving only the Genre
Explorer pills + Artist Map as the intentional splash elements.

64 script-integrity tests green.
2026-06-29 14:39:28 -07:00
BoulderBadgeDad
bf8e8beeb7 discover: convert the two artist sections to the .ya-card grid
Based On Your Listening + Recommended For You now use the same .ya-card as the rest (square cover,
name overlaid, recommendation reason as the subtitle) in wrapping grids with the Show-all clamp,
replacing the bespoke recommended-artist-card layout. The watchlist toggle and live image enrichment
are preserved by keeping their hooks on the new markup — .recommended-artist-card[data-artist-id],
.recommended-card-image, and .recommended-card-watchlist-btn (restyled as a hover-reveal corner pill
that stays lit when watching). _clampGrid wired into both onRendered callbacks.

64 script-integrity tests green.
2026-06-29 14:32:16 -07:00
BoulderBadgeDad
4cab44889c discover: unify album sections onto the .ya-card grid + Show-all clamp
Step 2 of the makeover — the album/release sections now use the same .ya-card as Your Artists
(square cover so art is not cropped, name overlaid, artist beneath) in responsive wrapping grids,
replacing the old spotify-library-card / discover-card / carousels:
- Recent Releases, Your Albums (owned/missing badge kept), Undiscovered Albums, New In Your Genres,
  From Your Labels (one shared _cacheDiscoverCard), Seasonal Albums.
- _insertCacheSection gained a wrapGrid flag; Genre Explorer opts out (wrapGrid=false) so its pill
  layout is not squashed into the card grid (fixes the regression where it broke under the hero).
- new _clampGrid caps a grid at ~12 cards with a Show all (N) / Show less toggle, so a 30-item
  section is not a wall; wired into the cache sections + Recent Releases + Seasonal Albums.

64 script-integrity tests green. Next: the two artist sections (Based On Your Listening / Recommended
For You) need their watchlist + live-enrichment hooks moved before they can switch to .ya-card.
2026-06-29 14:21:52 -07:00
BoulderBadgeDad
9c832ab3c3 discover: fold the remaining mixes into the "Your Mixes" shelf
Fresh Tape (release_radar), The Archives (discovery_weekly), and the Seasonal/Summer Mix
(seasonal_playlist) move out of their old full-width compact-track-table sections into the unified
shelf as mix cards (set the global in the controller renderItems, then _upsertMixCard + collapse the
old section after load). Daily Mixes loader is converted the same way (each mix → a card) for if/when
that HIDDEN feature is re-enabled. So every active mix now lives in one consistent shelf.

64 script-integrity tests green.
2026-06-29 13:56:27 -07:00
BoulderBadgeDad
9d5e58c4b8 discover: "Your Mixes" shelf — mixes are cards that open to a track list (Spotify-style)
Reworks the Discover page mixes from full-width compact-track tables into a consistent shelf of
playlist cards, the way Spotify Home does it. Each mix is ONE card (a 2x2 mosaic cover from its top
tracks + name/count, hover play button); clicking it opens a modal with the track list + the mix
actions — the track table now lives where a track list belongs (inside the opened mix).

- new "Your Mixes" section + .discover-mix-card / .discover-mixes-grid + .mix-modal styles.
- _buildMixCard / _upsertMixCard / openMixModal + a mix registry; the four personalized track-mix
  loaders (Listening Mix, Popular Picks, Hidden Gems, Discovery Shuffle) collapse their old table
  section into a card via _collapseOldMixSection (which also strips the old section's duplicate
  sync ids so the modal owns the live status).
- modal actions use the standard btn--secondary/btn--primary; Download closes the mix modal first
  so its own modal is interactable; Sync shows live progress IN the modal and survives close/re-open
  (detects an in-flight sync via discoverSyncPollers and re-reveals the status).

64 script-integrity tests green. Next: fold the remaining mixes (Fresh Tape / The Archives /
Seasonal / Daily Mixes) into the same shelf, then unify the album/genre sections onto the card.
2026-06-29 13:40:34 -07:00
BoulderBadgeDad
68d52a1b3f Release 2.8.2: version bump + release notes
Bumps base version 2.8.1 → 2.8.2 and the docker-publish default tag. A stability + performance
release: Spotify reliability (Docker boot-hang #949, the token-cache re-auth fix, on-demand
Sync-to-Spotify), the "slow after update" password-manager fix + Max Performance mode (#948), and
large-library imports that no longer time out the import page (#947).

Updates the five release touch-points: web_server version, docker-publish default, pr_description.md,
helper.js WHATS_NEW + VERSION_MODAL_SECTIONS (current release + Earlier-in-2.8.1 summary), and the
new RELEASE_2.8.2_discord.md (truncated for Discord, 1351 chars).
2026-06-29 11:10:50 -07:00
BoulderBadgeDad
f746fb4fc4 fix(#947): poll ALL staging queries while scanning, not just files
Re-review caught a real bug in the phase-2 polling: it refetched only the files query, but the
album import tab uses its OWN groups query (album-import-tab.tsx). So after a large-folder scan
completed, the album tab would stay stuck on its initial {scanning} response and never populate
(the singles tab was fine — it reads the polled files query). Switched the scan poll to
invalidateImportStagingQueries (files + groups + suggestions) so every mounted staging query
refetches when the scan finishes. (Suggestions is already async/cached, so it just no-ops.)

typecheck clean, scanning test still passes.
2026-06-29 10:03:14 -07:00
BoulderBadgeDad
8f1ffa7632 import: page shows scan progress + auto-loads when the background scan finishes (#947, phase 2 UI)
Frontend half of the async staging scan. The endpoints now return {scanning, progress} while a
large staging folder is still being scanned in the background; the page surfaces that and fills in
automatically when it completes — no manual refresh, no timeout.

- types: ImportStagingFiles/GroupsPayload gain optional scanning + progress (additive).
- useImportStaging exposes `scanning`/`scanProgress` and, while scanning, polls via a plain
  setInterval(refetch, 1500). Deliberately NOT react-query's refetchInterval — and a plain interval
  that only runs while scanning leaves the normal + error query states completely untouched.
- the header shows "Scanning N of M files…" instead of a count while the scan runs.

vitest: new test asserts the scan-progress header renders from a {scanning} response; typecheck
clean. Note: -route.test.tsx's pre-existing "staging files fail to load" test fails only in the
full-file run (passes in isolation) — verified it also fails on clean dev with all my changes
stashed, so it's a pre-existing test-isolation flake, not from this change.
2026-06-29 09:45:24 -07:00
BoulderBadgeDad
b05b641521
Merge pull request #948 from nick2000713/codex/fix-startup-freeze
Fix post-update UI freeze: stop password managers re-scanning the DOM + add Max Performance mode
2026-06-29 08:47:23 -07:00
BoulderBadgeDad
efefdd64ff spotify export: clickable authorize link (popup-block safe) + endpoint pre-check tests
Double-checking the on-demand auth flow: the needs_auth handler called window.open() AFTER an
await, which breaks the user-gesture chain so browsers popup-block it — the user would see "approve
in the new tab" with no tab. Replaced with a clickable authorize link (a direct click is never
blocked).

Adds two endpoint tests via the Flask test client: Spotify export returns needs_auth + the
/auth/spotify/export url (and short-circuits before the DB) when the token lacks write scope, and
does NOT short-circuit when write scope is present. 10 service-export tests green, 64 script-integrity
green, ruff clean.
2026-06-29 08:35:25 -07:00
BoulderBadgeDad
f3672c7ab4 spotify export: on-demand write-auth (restores Sync to Spotify safely, #945)
Brings back Spotify playlist export WITHOUT the regression that forced every user to re-auth.
The safety property: the global login scope (SPOTIFY_OAUTH_SCOPE) is NEVER changed, so no
existing token is invalidated. The write permission is requested only when a user actually
exports to Spotify.

- SPOTIFY_EXPORT_SCOPE = the global read scope + playlist-modify, used ONLY by the new
  /auth/spotify/export route. Spotify returns a superset token; the normal /callback exchanges
  and stores it unchanged (read ⊆ read+write keeps the standard auth check valid) — no callback
  changes needed.
- SpotifyClient.has_write_scope() checks the cached token for playlist-modify.
- start_playlist_export_service returns {needs_auth, auth_url} for Spotify when the token lacks
  write, instead of starting a doomed job. The modal opens the consent in a new tab and tells the
  user to retry once approved; the "Sync to Spotify" button is back, gated on connection as before.
- Release notes (pr_description / What's New / version modal / discord) restored to Spotify &
  Deezer with the one-time-permission note; discord back under 2000 chars (1983).

Tests: export scope is a strict superset of the (still read-only) global scope; has_write_scope
true/false for write/readonly/missing tokens and no-client. 275 spotify/oauth tests green, ruff
clean, 64 script-integrity green.
2026-06-29 08:32:19 -07:00
BoulderBadgeDad
6f451a34e1 playlist export: hide Spotify until on-demand write-auth; release notes → Deezer-now
Follow-up to the auth hotfix (633aa82b). The Spotify playlist-write scope was reverted out of the
global OAuth scope (it was force-invalidating every user's token on upgrade), so "Sync to Spotify"
can't get write access yet — clicking it would dead-end on a misleading "reconnect Spotify". So:

- removed the "Sync to Spotify" button from the export modal (Deezer stays); the backend write
  client + endpoint are left in place, dormant, for when on-demand write-auth lands
- modal copy is now Deezer-only ("Match missing tracks (Deezer)", "stored Deezer ID")
- release notes (pr_description, helper.js WHATS_NEW + version modal, RELEASE_2.8.1_discord.md)
  reworded from "Spotify & Deezer" to "Deezer", with a "Spotify export coming in a follow-up" note

64 script-integrity tests green; discord file back under the 2000-char limit (1952); no stale
Sync-to-Spotify mentions remain. Deezer export (live-verified) is unaffected.
2026-06-29 08:18:40 -07:00
nick2000713
bd6db37624 fix(ui): info icons show the button hand, not the text caret (Windows)
The settings info icons are role="button" spans with a text "i" glyph but no
cursor/user-select, so hovering the glyph gave the I-beam text caret on Windows
(Linux happened to resolve a pointer). Add cursor:pointer + user-select:none so
it reads as a button on every platform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:55:18 +02:00
nick2000713
8d549eb4fa fix(max-performance): hide the same decorative elements reduce-effects hides
Max Performance only neutralized animation/blur/shadow globally but didn't
replicate the reduce-effects-specific display:none rules, so with reduce-
effects OFF the sidebar aura orbs (.sidebar::before/::after) survived as two
hard static circles, the dash-card cursor-glow layers stayed, and nav-button
hover kept the expensive treatment. Depended on whether reduce-effects was on
before enabling Max Performance. Extend those three rule blocks to also match
body.max-performance — flash-free since the body class is server-rendered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:33:03 +02:00
nick2000713
1e68e339ca perf(bench): runtime toggle for password-manager autofill suppression
Expose window.__pmSuppress.disable()/enable() on the suppression IIFE so a
before/after benchmark can reproduce the pre-fix "before" state (managers
re-attach their autofill overlay) and restore it, without a rebuild. The
app itself never calls these; suppression stays on by default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:26:45 +02:00
nick2000713
84d6208bc9 perf: add Max Performance mode + stop password-manager autofill storm
Two CPU regressions surfaced in software-rendered / no-GPU containers
(Docker), where transform/opacity and canvas radial-gradient fills
rasterize on the CPU instead of a compositor:

1. Worker-orbs canvas + decorative motion saturate a core and freeze the
   UI. A new opt-in "Max Performance" mode is the nuclear low-power switch:
   body.max-performance CSS kills blur/shadow/filter AND all
   animation/transition (spinners go static), and JS halts every canvas
   loop (orbs, particles, cursor-glow, API sparks) via window._maxPerfActive.
   Reduce Visual Effects is now decoupled from the orbs — they follow their
   own toggle; only Max Performance force-kills them. While Max Performance
   is on, the Orbs/Particles/Reduce-Effects checkboxes lock + grey out, and
   save reads the runtime flags so prefs aren't clobbered.

2. Password managers (Bitwarden et al.) rebuild their autofill overlay on
   every DOM mutation; a captured trace showed Bitwarden using ~6x the CPU
   of the whole app (~400 setupOverlayOnField/sec). suppressPasswordManager-
   Autofill() tags non-credential inputs with data-bwignore / data-1p-ignore
   / data-lpignore / data-form-type=other so the managers skip them; real
   login/PIN fields are left alone.

Wired through: web_server.py (_initial_appearance_context), index.html
(inline flag + body class + checkbox), init.js (applyMaxPerformance +
bootstrap + listener + autofill suppression), settings.js (load/save),
worker-orbs.js / particles.js / api-monitor.js (gates), style.css.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:06:13 +02:00
BoulderBadgeDad
60e7193539 Release 2.8.1: version bump + release notes
Bumps base version 2.8.0 → 2.8.1 and the docker-publish default tag. Headline is the
Spotify/Deezer playlist export (#945); also the Library Reorganize rename-only mode (#875),
broader lossless handling (#941/#939), download + search fixes, the refined reduce-visual-effects
pass, and merged contributor PRs (#942/#943/#944).

Updates the five release touch-points: web_server version, docker-publish default, pr_description.md,
helper.js WHATS_NEW + VERSION_MODAL_SECTIONS (current release + Earlier-in-2.8.0 summary), and the
new RELEASE_2.8.1_discord.md.
2026-06-28 23:31:32 -07:00
BoulderBadgeDad
050ce79d51 playlist export: gate Spotify/Deezer buttons on connection (#945, increment 8)
The export modal now checks connection on open via /api/discover/your-albums/sources (cheap
token/ARL check, no live verify) and greys out + relabels any service that is not connected
("Not connected — set up X in Settings → Connections first"). Clicking a gated button nudges to
Settings instead of starting an export that would just fail with "not connected". The fetch runs
after the modal renders, so a slow/failed check never blocks the modal (buttons stay usable).

Pairs with the existing scope-403 handling: a Spotify token without playlist-modify still shows as
connected (it IS), and the writer returns the clear "Reconnect Spotify to grant playlist write
access" message — so "not connected at all" and "connected but needs reconnect for write" are both
covered. Static file, no rebuild.
2026-06-28 23:04:20 -07:00
BoulderBadgeDad
4743dfd644 playlist export: opt-in confident-search backfill for the unmatched tail (#945, increment 7)
Adds the third resolver stage for tracks the discovery cache + library can't resolve — a live
search of the target service, gated behind a "Match missing tracks" toggle so the API cost is opt-in.

The whole point is coverage WITHOUT the wrong-track risk, so it's a CONFIDENT match, not "search
and grab":
- search_service_track_id(artist, title, search_fn): searches the service, reranks via the existing
  relevance scorer (filter_and_rerank), and returns the top hit's id ONLY if it clears
  BACKFILL_MIN_SCORE (1.2 on the score_track scale). A wrong-artist hit (no 1.5x exact-artist boost,
  caps ~1.0) or a karaoke/cover (x0.05) can't clear the floor → None, and the track is left out
  rather than added wrong. search_fn injected → unit-testable without a live service.
- resolve_service_track_ids gains an optional search_id_fn: cache → library → search. Tallies
  from_search separately.
- _run_service_export builds the search fn from the service's metadata search client only when
  job['backfill'] is set; the endpoint reads `backfill` from the body; the modal adds the toggle and
  the status line shows "(N matched live)".

Store-back of confident matches deferred: a mirrored-only track may have no library row to write to,
so persisting needs the track→library mapping — a follow-up, not correctness.

9 new tests incl. the safety ones: wrong-artist rejected, karaoke/cover rejected, real-over-cover
picked, fail-safe on search error, and the cache→library→search waterfall + toggle wiring (on/off).
28 export/orchestration tests green, 64 script-integrity green, ruff clean.
2026-06-28 23:02:08 -07:00
BoulderBadgeDad
91eaaeabb2
Merge pull request #944 from HellRa1SeR/fix-vulnerabilities
fix(webui): resolve npm audit vulnerabilities via audit fix
2026-06-28 22:30:15 -07:00
BoulderBadgeDad
614deba0af playlist export: Sync to Spotify / Deezer modal options (#945, increment 5)
Adds "Sync to Spotify" and "Sync to Deezer" buttons to the mirrored-playlist export modal
(#pl-export-modal), alongside the existing ListenBrainz/JSPF options. They POST to the new
/export/service/<service> endpoint; the shared poller now reports service progress
("Pushing to Spotify…"), the matched/unmatched count, and links the created playlist on
done. ListenBrainz/JSPF paths unchanged. Static file, no rebuild.

This completes the #945 vertical: resolver → Spotify+Deezer write clients → export job +
endpoint → modal. Reverse-sync to a service is a clean export (uses the stored per-track
service IDs from enrichment), distinct from true bidirectional sync.
2026-06-28 21:22:30 -07:00
BoulderBadgeDad
c593a17ac2 spotify search endpoint: plain query, not field-scoped — fixes pool-fix "no results"
The Wing It pool "Fix Match" search returned "no results" for everything (even obvious
tracks). Root cause: /api/spotify/search_tracks built a Spotify field-filtered query
(track:X artist:Y) and handed it to spotify_client.search_tracks, which falls back to the
user's configured source when official Spotify isn't serving the request. The fallback
(Deezer here) got the raw Spotify `track:…artist:…` syntax it can't parse and aborted the
connection (RemoteDisconnected) — so the user's perfectly working Deezer failed ONLY on
this path, on this query format. The iTunes and Deezer search endpoints already dropped
field syntax for exactly this reason; the Spotify one was the lone holdout.

fix:
- new pure helper relevance.build_combined_search_query(track, artist, legacy) — plain,
  source-agnostic query; documents WHY field syntax is wrong here. the endpoint already
  reranks by expected title/artist, so precision is recovered without the brittle syntax.
- the Spotify endpoint uses it (now consistent with iTunes/Deezer).
- frontend (searchPoolFix): surface the real error (auth / 500 / upstream abort) instead
  of masking everything as "No results found" — which is what made this undiagnosable.

5 helper tests incl. the regression (output must contain no 'track:'/'artist:' syntax).
654 metadata/search tests green, 64 script-integrity green, ruff clean.
2026-06-28 19:48:04 -07:00
BoulderBadgeDad
62aa2bef2d library reorganize: Full vs Rename-only action in the modal (#875)
Adds an "Action" selector to the reorganize modal — "Full reorganize (default)" vs
"Rename only (skip post-processing)" — with a hint explaining rename-only skips
re-tagging/quality/AcoustID, only touches files whose name changes, and that renaming
can reset media-server play counts / date-added. executeReorganize sends rename_only in
the apply POST. Default is full → existing behaviour unchanged. Static file, no rebuild.
2026-06-28 19:11:03 -07:00
Siddharth Pradhan
ad715dddda fix(webui): resolve npm audit vulnerabilities via audit fix - vite HIGH: server.fs.deny bypass and NTLM hash disclosure on Windows - undici HIGH: TLS bypass, WebSocket DoS, cross-origin SOCKS5 routing - @babel/core LOW: arbitrary file read via sourceMappingURL comment All 90 frontend tests (16 suites) continue to pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 21:22:42 -04:00
BoulderBadgeDad
34adb6fb32 worker orbs: run at ~30fps when reduce-effects is on (perf)
Pairs with the previous commit (orbs now run under reduce-effects). When the user has asked
for performance (reduce-effects on) we don't need the orbs at 60fps — the slow drift and sparks
are indistinguishable at 30, and dropping every other render roughly halves the per-frame canvas
cost, keeping the "orbs under reduce-effects" experiment cheap.

The canvas still ticks at 60fps and frameCount still increments every tick, so `time` stays
real-time and the drift speed is unchanged — we just draw it half as often. Precedence: the
existing fully-asleep ~20fps throttle still wins; the 30fps cap only applies awake + reduce-effects.
Chrome users with full effects keep 60fps — no reason to dim them.
2026-06-28 17:35:46 -07:00
BoulderBadgeDad
79383df6d8 worker orbs: let the toggle win over reduce-effects (experiment)
Reduce-effects used to force-kill the worker orbs (isEnabled() had && !_reduceEffectsActive),
which also made the orb toggle a dead setting whenever reduce-effects was on.

The assumption was "the orbs ARE the expensive thing." On inspection that looks wrong: the
dashboard orb glow is drawn with canvas radial gradients, not a CSS blur(28px). The genuinely
expensive blur is the SIDEBAR aura orbs + frosted glass (CSS filters), which reduce-effects
still kills via filter:none regardless. So the orb canvas's per-frame cost should be moderate,
not the blur-rasterize lag.

So decouple them: the worker-orbs toggle controls the orbs on its own; reduce-effects keeps
killing the expensive CSS rendering but no longer gates the orbs. This also fixes the dead-toggle
conflict (the orb toggle now works under reduce-effects instead of being silently overridden).

Empirical: try it and watch the dashboard CPU. If the orb canvas under reduce-effects pushes it
back up, revert is one token — re-add `&& !window._reduceEffectsActive` to isEnabled().
2026-06-28 17:29:22 -07:00
BoulderBadgeDad
a62d2d4310 ui appearance: default worker orbs OFF on Firefox for first-time users
The blurred 60fps worker-orb canvas is the main remaining Firefox lag source after the
#935 sweep (multiple Discord lag reports). So for a FIRST-TIME user with no saved
preference, default the orbs OFF on Firefox (smooth first impression where it's needed)
and ON everywhere else (full polish where the browser handles it). An explicit saved
choice ALWAYS wins — this only picks the default when the user hasn't chosen.

Done kettui-style with a SINGLE source of truth, not the dual browser-detection I first
floated (server UA + client _isFirefox would be the same fact in two places that can
drift — exactly the server/client class #943's green-flash fix just cleaned up):

- core/ui_appearance.py (new, pure + importable): is_firefox_user_agent +
  resolve_worker_orbs_default(explicit, is_firefox) — explicit wins, unset → !firefox.
- web_server: the SERVER decides (UA via _request_is_firefox, request-context-safe) and
  injects initial_worker_orbs_enabled; config default flipped None so "unset" is
  distinguishable from an explicit False. The client just consumes the injected value
  (init.js unchanged) — no client-side re-derivation of "is Firefox".
- settings.js: the orb checkbox default now reflects the server value when unset, so
  saving Settings can't silently flip a first-time Firefox user's orbs back on.

No regression: Chrome users unchanged; users with an explicit setting unchanged (it
wins regardless of browser); /api/settings returns raw config so it can't clobber the
default for an unset value. Verified end-to-end through a real Flask request context
(Firefox→off, Chrome→on, explicit wins both ways, no crash outside a request). 8 pure
seam tests pin the contract; ruff clean.
2026-06-28 16:50:22 -07:00
BoulderBadgeDad
3207310448 reduce-effects: kill expensive GPU properties, stop freezing functional motion
"Reduce visual effects" was a sledgehammer: body.reduce-effects * forced
animation:none + transition-duration:0s on every element. That froze every CSS loading
spinner mid-rotation — including the dash-header worker-service spinners (musicbrainz /
spotify / deezer / … .active .<svc>-spinner) — which read as BROKEN rather than "off".
It also killed cheap hover feedback like the Quick Actions buttons.

The actual lag (esp. Firefox, see the #935 sweep) is backdrop-filter / box-shadow /
filter re-rasterizing every frame — NOT the animations themselves. Transform- and
opacity-only motion (the spinners) composites for ~free.

So: keep forcing the expensive properties to none (unchanged — that's the real fix),
but drop the blanket animation/transition kills. !important author declarations outrank
animation + transition declarations in the cascade, so any keyframe/transition that
tries to set blur/shadow/filter is still neutralized even while it runs — the spinner
spins, just without the glow. Net: functional spinners stay alive, Quick Actions hover
(transform + border-colour) returns, box-shadow transitions are no-ops (shadow forced
none), and the GPU-heavy rendering that caused the lag stays gone. The worker-orb CANVAS
is unaffected (JS-gated separately) and stays off under reduce-effects, as intended.

Static guard test pins the contract: the global rule must keep the expensive-property
kills and must NOT reintroduce blanket animation:none / transition:0s.
2026-06-28 16:22:10 -07:00
BoulderBadgeDad
6a388c43fc
Merge pull request #943 from nick2000713/ui/settings-page-cleanup
UI/settings page cleanup
2026-06-28 16:05:38 -07:00
dev
33fe92a525 Enlarge settings page header 2026-06-29 00:37:01 +02:00
dev
512ec227be Polish security settings controls 2026-06-29 00:33:54 +02:00
dev
caee0fc3e2 Clarify import quality and AcoustID wording 2026-06-29 00:19:53 +02:00
BoulderBadgeDad
969700674c import singles: default the Identify search to "artist - title" (dash)
Discord (Shdjfgatdif): the import-singles Identify search prefilled "artist title" (space), so
"Sub Focus Last Jungle" returned junk while "Sub Focus - Last Jungle" found the track. The
placeholder already hints "Search artist - title..."; the prefill just did not match it. Join with
" - " instead of " ". filter(Boolean) keeps a lone title (no artist) dash-free.
2026-06-28 15:16:13 -07:00
dev
1382cb6117 Bootstrap saved appearance effects early 2026-06-29 00:01:56 +02:00
dev
8149f35fae Refine settings header layout 2026-06-28 23:57:26 +02:00
dev
1da677ee2d Keep settings logs viewer full width 2026-06-28 23:45:49 +02:00
dev
b43e44219a Merge remote-tracking branch 'upstream/main' into ui/settings-page-cleanup
# Conflicts:
#	webui/static/style.css
2026-06-28 23:29:17 +02:00
dev
0276aa8764 Update settings page overhaul 2026-06-28 22:52:09 +02:00