Commit graph

571 commits

Author SHA1 Message Date
Broque Thomas
4ae5aee528 Sync page: collapse tabs to brand-logo chips with active label pill
The sync-tabs row had 14 sources jostling for horizontal space —
labels wrapped to 2 lines, the active pill ate disproportionate
room, the whole strip felt cramped and would only get worse as
more sources get added.

Restyled the strip as circular brand-logo chips. Inactive tabs
are 40px discs that show only the source's icon; the currently-
active tab swells into a pill that reveals its label inline.
Hover surfaces the source name as a native tooltip via the
title attr. Each chip carries its source's brand color as a
hover ring + active fill (Spotify green, Tidal orange, Qobuz
blue, Deezer purple, iTunes coral, YouTube red, Beatport green,
LB orange, Last.fm red, SSD teal).

Three sources share a logo with another source (Spotify Link
/ Spotify, Deezer Link / Deezer, iTunes Link / no native iTunes
but same logo family). Each "Link" variant carries a small
chain-link badge bottom-right so the chip disambiguates without
forcing the label to always be visible.

CSS-only swap — same JS handlers, same .active class, same
data-tab routing. HTML edit wraps each tab's label in a
``<span class="sync-tab-label">`` and adds ``data-link="true"``
to the Link variants so the CSS can target them.

Responsive: chips collapse to 36px on laptop / tablet and 32px
on mobile; the divider hides on mobile and gap tightens.
2026-05-27 22:04:28 -07:00
Broque Thomas
b19c1ae8cc Auto-Sync sidebar: brand logo on each source-group header
The sidebar source-group headers (Spotify / Tidal / Qobuz / Deezer /
YouTube / Last.fm Radio / ListenBrainz / iTunes Link / SoulSync
Discovery / Spotify Link) only showed the source name in caps —
the dashboard equalizer + connections panels both render the
actual brand logo, so the sidebar reading as text-only felt
disconnected.

Added a small (18px) circular brand-logo chip to the left of
each source-group title, sourced from the same URLs the
dashboard equalizer avatars use. Dark glass backdrop + accent
ring + drop-shadow on the logo so the chip stays legible
against either light or dark marks; brightness(0) invert(1)
applied to Tidal / Qobuz / iTunes-Link so their dark-foreground
marks render as white silhouettes against the disc (same
recipe the equalizer overrides use). Last.fm's square avatar
PNG clips to a circle via object-fit: cover.

Sources without a publicly available logo (Beatport, file
imports) fall through to no-chip — the <img onerror> swap
hides the broken image so the header still renders cleanly.
2026-05-27 20:23:12 -07:00
Broque Thomas
e296fbfadd Auto-Sync manager: full visual overhaul to match the dashboard vibe
The Auto-Sync manager modal had been carrying its original visual
treatment forward unchanged while the rest of the app moved
toward the glassy / accent-radial / gradient-border aesthetic
the dashboard now sets. Restyled every surface inside the modal
to match.

Strategy: selector-based override layer appended at the end of
``webui/static/style.css`` — every selector in the new block
already exists in the original CSS above; the new block wins on
cascade order. Zero HTML / JS changes; functionality untouched.
Delete the v2 block to revert.

Surfaces restyled
- Modal shell: glass + thin accent border + corner radial wash
  + inner top-edge highlight, matching the dashboard ``.dash-card``
  architecture
- Header: gradient-clipped title, accent-tinted eyebrow, hairline
  accent separator below, spinning-X close button
- KPI summary tiles: dashboard-style gradient tiles, accent
  top-edge glow on hover, gradient stat numbers
- Live monitor strip: accent-tinted glass card, status-colored
  borders (running = green, error = red)
- Refresh / intro buttons: pill primary with accent fill + glow
  on hover (replaces the bare ghost button)
- Tabs: underline-style with accent fill + soft radial glow on
  the active tab (replaces the pill-tab look)
- Sidebar: glass panel, source groups as collapsible-feel cards,
  accent border on scheduled playlist tiles, accent ring on the
  filter input focus
- Board: subtle accent radial spotlight backdrop; columns are
  glass cards with gradient headers + accent drag-over glow
- Drop zones: animated dashed pill with accent radial wash;
  accent-tinted text on drag-over
- Scheduled cards: accent left-edge stripe, gradient pill timing
  badges, pill "Run now" primary + rotating ghost X — health
  variants (failing / warning) re-tint the left-edge stripe
- Run history rows: dashboard recent-activity aesthetic, accent
  hover lift, pill status badges with colored borders
- Bulk schedule popover: glass card with accent border, pill
  buttons, red ghost for unschedule
- Weekly editor: glass modal matching version-modal vibe,
  day-toggle pills (accent fill when active), pill save button
- Empty / monitor-empty states: dashed glass card with subtle
  vibe
- Soft accent-tinted scrollbars throughout the modal
2026-05-27 20:15:56 -07:00
Broque Thomas
a315192e9a Dashboard: round Last.fm avatar, invert dark-mark logos (Tidal/Qobuz/Discogs/Amazon)
Last.fm ships a square Twitter avatar; clip it to a circle so the
disc reads as a uniform chip. Tidal / Qobuz / Discogs / Amazon
ship dark-foreground marks that disappear against the dark glass
avatar backdrop — invert to a white silhouette so the logo
actually reads. The per-service accent drop-shadow still applies
so the brand color cue is preserved as a glow around the white
silhouette.
2026-05-27 19:32:32 -07:00
Broque Thomas
79465580ab Dashboard: equalizer bars now show real brand logos in the avatar disc
Initial-letter glyphs (SP / AM / DZ / ...) read as placeholder
once the brand-logo equalizer disc was visualised — each chip
should carry the service's actual mark. Wired the same logo
URLs the header-action worker orbs already load (Spotify press
asset, iTunes Wikimedia SVG, Deezer brandfetch symbol, Last.fm
avatar, Genius logo, MusicBrainz Wikimedia SVG, AudioDB local
PNG, Tidal / Qobuz / Discogs SVGRepo marks, Amazon local SVG)
into a new _RATE_GAUGE_LOGOS map and rendered as an ``<img>``
inside the avatar disc.

Visual details
- Disc backdrop switched from a solid accent-gradient fill to a
  dark glass radial + accent-tinted ring + accent drop-shadow
  on the logo. The service color still anchors the chip without
  competing with the logo for contrast.
- Logo sized at 75% of the disc for breathing room. Drop-shadow
  pops dark / multi-tone marks against the dark backdrop.
- Avatar bumped to 34px / 28px / 26px across desktop / tablet /
  mobile so logos read clearly at every breakpoint.

Resilience
- ``<img onerror>`` swaps in an initial-letter glyph span on
  load failure (CDN drop, network blip). The ``.rate-eq-avatar
  --fallback`` variant restores the original accent-gradient
  disc look so the fallback chip still reads as branded.

Asset
- AudioDB ships no public logo URL — saved the existing header-
  action base64 PNG (~30 KB) to ``webui/static/audiodb.png`` so
  the equalizer can reference it as ``/static/audiodb.png`` like
  Amazon already does.
2026-05-27 19:22:28 -07:00
Broque Thomas
c845fa933f Dashboard: next-level polish on enrichment equalizer bars
Four upgrades that take the equalizer row from clean to vibey.
All tied together by the same --eq-accent / --eq-glow CSS
variables so future tweaks stay coherent across the four
animation layers.

1. Brand-color avatar disc above each bar. Circular chip with a
   2-3 letter glyph (SP / AM / DZ / LF / GN / MB / ADB / TD /
   QB / DC / AZ) and a radial gradient using the service's
   accent. Inner highlight + drop-shadow for depth; slow halo
   pulse when the worker is running. Anchors each capsule to
   its identity so the row reads as "these are your services"
   not "these are 11 anonymous bars."

2. Peak-flash detector. When ``cpm`` actually steps upward
   between socket updates (above a small jitter floor so
   near-zero noise doesn't trigger), the peak tip briefly
   flares white-hot, the fill flashes brighter, and the
   reflection puddle ripples — all on a 650ms one-shot the JS
   removes after fire. Mimics a hardware VU meter's peak-
   detect LED. Sells the "alive" feeling by tying bar
   movement to real call activity, not just continuous
   animation.

3. Rolling-counter number animation. The live count under
   the bar digit-animates from old→new with easeOutCubic
   over 520ms instead of snapping. Per-element animation
   handles tracked in a WeakMap so a fast second update
   cancels the prior RAF loop instead of fighting it.
   Premium-counter feel.

4. Glass-surface reflection puddle. Soft accent-colored
   blurred ellipse under each bar; opacity scales with the
   real (unclamped) rate via the --eq-glow variable so idle
   bars don't pollute the row with permanent ground-light.
   Rate-limited bars get a red puddle. Peak-flash briefly
   intensifies the puddle so the surface "ripples" with the
   call burst. Mounted on the host button (not the track) so
   it escapes the track's overflow clipping.

Responsive: avatar disc shrinks to 26px at laptop/tablet,
24px at mobile.
2026-05-27 19:08:00 -07:00
Broque Thomas
74dcafd6e9 Dashboard: equalizer-bar redesign for Enrichment Services panel
The rate monitor on the dashboard used a 10-column grid of circular
SVG speedometers. With 11 services configured (Amazon was the
straw), the grid produced 10-in-row-1 + 1-orphan-in-row-2, breaking
the dashboard's tile symmetry. Speedometers also wasted ~80% of
their pixels on empty arc — most services sit at 0 cpm most of
the time, so the row visually read as a wall of empty gauges.

Replaced with a VU-meter / equalizer row: one vertical capsule
per service, brand-color gradient filling from the bottom, bar
height tracks ``calls/min ÷ limit``. Music-app native aesthetic,
fits the existing accent-heavy glassy vibe, and symmetric by
design at any service count — services slot into the flex row.

Visual details
- 4% sliver floor on idle bars so the row reads as "everything
  alive" instead of "8 dead gauges" — vibe over literal zero
- Continuous shimmer scan when worker is running (vertical wash)
- Slow breathing pulse on idle bars
- Red gradient + faster pulse when rate-limited
- White-hot peak tip glows in the service's accent color
- Status pill below each bar (Running pulses green, Paused amber)
- Big count number floats top-center of the track

Behavior
- Click any bar opens the same detail modal the speedometer used —
  no data-flow changes, no API changes, drop-in visual swap.
- Renderer auto-detects the dashboard context (data-card="enrichment")
  and routes through the equalizer path; legacy speedometer code
  still ships for any non-dashboard mount.
- Responsive: tightens at laptop/tablet breakpoints, wraps to
  5-per-row on phones.
2026-05-27 19:02:50 -07:00
Broque Thomas
17607c2d83 Update style.css 2026-05-27 18:34:07 -07:00
Broque Thomas
698c21c3ce Auto-Sync Weekly Board: weekday schedules in the UI (PR 3/4)
PR 3 of the schedule-types feature — see
``memory/project_auto_sync_schedule_types.md``. Backend
``next_run_at`` + ``weekly_time`` trigger handler landed in PRs 1-2.
This PR exposes them in the Auto-Sync manager so users can finally
schedule playlists by day-of-week + time instead of only hourly
intervals.

**UI layout:**

The Auto-Sync modal grows a ``Weekly Board`` tab between
``Hourly Board`` (renamed from ``Schedule Board``) and
``Automation Pipelines``. Same sidebar (mirrored playlists grouped
by source, with filter). Main panel is 7 day columns Mon-Sun
instead of 10 hour buckets. Drag a playlist onto a day column →
creates a single-day weekly schedule at the default time
(09:00 in the browser's IANA tz from
``Intl.DateTimeFormat().resolvedOptions().timeZone``). Click any
scheduled card → opens an editor popover for time, multi-day
toggles, tz override, and unschedule.

Multi-day schedules render under every matching column (Mon-Wed-Fri
schedule appears as three cards, one per column) — matches how
users think about "this playlist runs on Mon AND Wed AND Fri".

**Mutual exclusion:** one schedule per playlist. The save path on
either tab deletes any existing schedule of the OTHER kind before
installing the new one. Backend can technically run both as two
separate automation rows, but two cards under the same playlist
would surprise users and the engine has no merge semantic for
"daily-and-hourly".

**Pure-function helpers** (testable via node:test, matching the
existing ``tests/static/test_auto_sync.mjs`` pattern):

- ``detectBrowserTimezone()`` — Intl tz with UTC fallback for
  browsers where Intl is absent.
- ``autoSyncWeeklyTrigger({time, days, tz})`` — defensive payload
  builder: garbage time → 09:00, unrecognised days dropped,
  missing tz → browser tz.
- ``autoSyncWeeklyFromTrigger(config)`` — inverse parser with
  the same defensive shape. Empty days expands to every weekday
  (matches ``next_run_at`` engine semantic). Returns null for
  non-object configs so ``buildAutoSyncScheduleState`` can route
  broken rows to automationPipelines instead of silently
  bucketing them as every-day weekly.
- ``autoSyncWeeklyLabel(parsed)`` — sorted "Mon, Wed, Fri @
  09:00" / collapses to "Daily @ HH:MM" for full-week / "Unscheduled"
  for null. Canonical Mon-Sun ordering regardless of input order.

**Tests:** 26 new node:test cases across ``detectBrowserTimezone``
x1, ``autoSyncWeeklyTrigger`` x6, ``autoSyncWeeklyFromTrigger`` x6,
``autoSyncWeeklyLabel`` x5, and ``buildAutoSyncScheduleState``
weekly bucketing x5 (covering owned weekly_time → weeklySchedules,
hourly stays in playlistSchedules, non-owned falls through to
automationPipelines, legacy-named auto-sync rows still recognised,
garbage trigger_config falls through). All 62 node:test cases pass;
261 across the automation pytest suite still green (zero regression
on PRs 1-2's plumbing). Python wrapper at
``tests/test_auto_sync_js.py`` shells out cleanly.

**CSS** (themed to the existing Auto-Sync gradient + accent
variables):
- 7-column grid for the weekly board, narrower than the 10
  hour-bucket layout.
- Editor popover with backdrop-blur, accent-tinted save / delete
  buttons, hover states that pick up the user's accent color.
- ``scheduled-elsewhere`` state for playlists with an hourly
  schedule visible on the weekly board (dashed border + opacity)
  so the user knows a drop will replace, not stack.

**WHATS_NEW entry** under 2.6.3 unreleased — first user-visible
slice of the schedule-types feature.

PR 4 (Monthly UI tab) deferred until weekly proves wanted.
2026-05-27 12:39:56 -07:00
Broque Thomas
80a88a62ac Auto-Sync sidebar: improve playlist card readability
The mirrored-playlist cards in the Auto-Sync schedule modal's
sidebar were truncating long names with ellipsis on a single line
+ rendering meta info at 10px, which made entries like
"Top Missed Recordings of 2024 for Nezreka" or "ListenBrainz
Weekly Exploration" unreadable.

- Name wraps to multiple lines instead of ellipsis-truncating
  (sidebar is narrow; truncation hid critical disambiguating
  text like the year / week / username).
- Bumped name 12px → 13px, meta 10px → 11px with brighter color
  (0.4 → 0.55 alpha).
- Bumped card padding 10px/12px → 12px/14px + spacing 6px → 8px
  so multi-line entries have breathing room.
- Pinned the leading status dot to the first text line via
  ``margin-top`` so multi-line names flow underneath rather than
  push the dot off-center.
2026-05-26 20:12:15 -07:00
Broque Thomas
bd91c94f92 Add SoulSync Discovery tab to Sync page (Phase 1c.3)
Last of the three unified-tab phases. Surfaces the user's
persisted personalized playlists (decade mixes, hidden gems,
popular picks, daily mixes, discovery shuffle, etc.) on the
Sync page so they participate in the mirrored-playlist +
Auto-Sync pipeline like every other source.

Different shape from the LB / Last.fm tabs:

- Tracks already carry Spotify / iTunes / Deezer IDs (matched
  at generation time from the discovery pool), so there is NO
  MB-style "needs discovery" hop. The mirror is created with
  fully-populated ``matched_data`` JSON inline, downstream
  consumers (sync, wishlist) see canonical extra_data
  immediately.
- Click on a card runs the kind's generator
  (``POST /api/personalized/playlist/<kind>/<variant>/refresh``)
  + grabs the fresh track snapshot + mirrors under a synthetic
  id of the form ``ssd_<kind>_<variant>`` (e.g. ``ssd_decade_1980s``,
  ``ssd_hidden_gems``). Re-clicks UPSERT the same row, so the
  Auto-Sync schedule survives every refresh.
- Sub-tabs / archive concept don't apply here — each personalized
  playlist is already a singleton per (profile, kind, variant);
  the manager handles its own rotation.

New file: ``webui/static/sync-soulsync-discovery.js`` (~210 lines).
``initializeSyncPage`` learns a new tab branch. CSS adds
``soulsync-discovery-icon`` (star SVG, teal ``#14b8a6``) +
``.soulsync-discovery-playlist-card`` joins the unified card
selector group with a matching teal accent.

WHATS_NEW entry added under 2.6.3.

236 tests still green; no Python paths touched.
2026-05-26 19:46:03 -07:00
Broque Thomas
38e35930a9 Add Last.fm Radio tab to Sync page (Phase 1c.2)
Sibling to the ListenBrainz Sync tab from Phase 1c.1. Last.fm Radio
playlists already live in the same ``listenbrainz_playlists`` table
as LB ones (``playlist_type='lastfm_radio'``) and run through the
same MB-track discovery worker, so this tab is intentionally thin
— list + render + delegate. Card click hands straight off to the
LB Sync-tab click handler since the downstream modal + state
machine are identical.

- ``webui/index.html``: new ``<button data-tab="lastfm-sync">``
  + tab content container between the LB tab and the existing
  Import / Mirrored tabs. Plus a ``<script>`` tag for the new
  module.
- ``webui/static/sync-lastfm.js`` (new): ``loadLastfmSyncPlaylists``
  hits the existing ``/api/discover/listenbrainz/lastfm-radio``
  endpoint, ``renderLastfmSyncPlaylists`` mirrors the LB card
  shape with a ``📻`` icon + a ``.lastfm-playlist-card`` brand
  class, click handler forwards to
  ``handleListenBrainzSyncCardClick``.
- ``webui/static/sync-listenbrainz.js``: the shared 500ms refresh
  loop now iterates LB + Last.fm cards in one pass and treats
  either tab as "active" for liveness. No second loop needed.
- ``webui/static/sync-services.js``: new tab-activation branch in
  ``initializeSyncPage`` mirrors the LB pattern.
- ``webui/static/style.css``: ``.lastfm-icon`` SVG (Last.fm "as"
  logo, red), and ``.lastfm-playlist-card`` joins the unified
  card selector group with the Last.fm-red accent
  (``rgba(213, 16, 7, ...)``).
- ``web_server.py``: the lastfm-radio endpoint now includes
  ``track_count`` in its JSPF payload (same fix as the LB
  endpoints last commit).
- WHATS_NEW entry added under 2.6.3.

Mirrors created from Last.fm radios participate in the same auto-
trim Phase 1c.1's cascade-delete hook does — when the LB manager
rotates a stale ``lastfm_radio`` row out of its 5-most-recent
window, the matching ``source='lastfm'`` mirror row is removed
along with it. Library files stay on disk.

225 tests across adapter + automation suites still green; this
commit adds no Python paths to test.
2026-05-26 15:24:23 -07:00
Broque Thomas
969d5ffc1b Fix LB Sync tab card styling — dead CSS + ID collision
Two interacting bugs that left LB Sync-tab cards rendering with a
solid orange gradient background instead of the dark glass style
every other Sync-page card uses:

1. **Duplicate element id** ``listenbrainz-tab-content``: the new
   Sync-tab content div reused the same id the Discover page's
   pre-existing LB section already owned. Two elements with the
   same id is invalid HTML, and ``getElementById`` in the refresh
   loop was hitting the Sync version first while ``initialize
   SyncPage``'s ``${tabId}-tab-content`` lookup could race against
   it. Renamed the Sync-page tab id + ``data-tab`` attribute to
   ``listenbrainz-sync`` (matches the existing ``${tabId}-tab-
   content`` convention so the lookup becomes
   ``listenbrainz-sync-tab-content``). Discover-page LB tab
   keeps its original id untouched.

2. **Dead ``.listenbrainz-playlist-card`` rule** at style.css
   L36155 painting a solid ``linear-gradient(#eb743b → #d26230)``
   over the card. That class was orphaned — no JS or HTML
   instantiated it before Phase 1c.1 — but it sat at higher
   source order than my unified ``.youtube-playlist-card,
   .tidal-playlist-card, ...`` rule, so the bare-class selector
   won the cascade and overwrote the dark glass background.
   Also removed the matching dead ``.listenbrainz-icon { font-
   size: 48px }`` rule and its local ``@keyframes pulse`` copy
   (the keyframes are defined in four other live blocks).

3. **Missing LB selectors in unified inner-element rules**:
   ``.listenbrainz-playlist-card`` was only added to the OUTER
   card selector group in the first pass — the inner
   ``.playlist-card-icon`` / ``.playlist-card-content`` /
   ``.playlist-card-name`` / ``.playlist-card-info`` /
   ``.playlist-card-action-btn`` (+ ::before, :hover, :disabled)
   selector groups were left out, so the inner elements lost all
   their styling. Bulk-added LB to every group so the card
   inherits the full glass shell the other sources get, with a
   brand-orange ``rgba(235, 116, 59, ...)`` accent matching the
   Tidal / Deezer / Spotify-public pattern.
2026-05-26 14:41:57 -07:00
Broque Thomas
df31d42b94 Fix LB Sync tab card data shape + tone down styling
Two bugs from the initial LB tab commit (a7053a60):

1. **All cards showed identical "ListenBrainz Playlist / 0 tracks"
   defaults.** The /api/discover/listenbrainz/* endpoints wrap each
   entry in JSPF shape — ``{playlist: {identifier, title, creator,
   annotation, track}}`` — but renderListenBrainzSyncPlaylists was
   reading ``p.title`` / ``p.creator`` / ``p.track_count`` directly,
   so every field hit its fallback. Now unwraps the inner playlist
   object, extracts the MBID from the identifier URL via
   ``.split('/').pop()`` (matches buildListenBrainzPlaylistsHtml on
   the Discover page), and reads track_count from
   ``annotation.track_count`` with a fallback to ``track.length``.

2. **The tab looked too orange.** The initial commit gave the
   sub-tabs a saturated orange surface that clashed with the rest
   of the app, and the new ``.listenbrainz-playlist-card`` class
   wasn't in the unified ``.youtube-playlist-card,
   .tidal-playlist-card, ...`` selector group — so the card lost
   its dark glass base and inherited only my override CSS. Two
   fixes:

   - Added ``.listenbrainz-playlist-card`` to the unified card
     selector group (base + ::before + hover + hover::before + icon)
     so it picks up the dark glass background. The brand accent
     stripe + hover glow use ``rgba(235, 116, 59, ...)`` matching
     the other source cards' subtle accent pattern.
   - Sub-tabs reverted to a neutral dark surface (``rgba(255,
     255, 255, 0.04)``) with the orange used only as a thin
     accent on the active state's border + inset shadow.
   - Dropped the ``.refresh-button.listenbrainz`` override so the
     refresh button falls back to the user's chosen accent like
     the Spotify / Qobuz refresh buttons do.
2026-05-26 14:25:01 -07:00
Broque Thomas
a7053a6061 Add ListenBrainz tab to Sync page (Phase 1c.1)
First user-facing slice of the Discover-to-Sync unification. Adds a
ListenBrainz tab on the Sync page alongside Tidal / Qobuz /
Spotify Public / Beatport / etc. so users can mirror + auto-sync
ListenBrainz playlists from the same surface as every other source,
without detouring through the Discover page.

The Discover-page LB flow already owns all the heavy lifting
(state machine, discovery polling, sync → mirror creation). This
commit adds the Sync-page entry point only — list cached LB
playlists, render cards, pre-fetch tracks on click, hand off to
``openDownloadModalForListenBrainzPlaylist``. Zero backend changes.

- ``webui/index.html``: new ``<button data-tab="listenbrainz">`` +
  tab content container with "For You / My Playlists /
  Collaborative" sub-tabs and a refresh button.
- ``webui/static/sync-listenbrainz.js`` (new): ``loadListenBrainz
  SyncPlaylists`` fetches all three LB cache categories in parallel,
  ``renderListenBrainzSyncPlaylists`` renders cards in the standard
  ``.youtube-playlist-card`` shell with the existing phase-state
  helpers (so card colors / button text stay consistent with Tidal
  / Qobuz / etc.). Click handler populates the
  ``listenbrainzTracksCache`` from
  ``/api/discover/listenbrainz/playlist/<mbid>`` if not already
  primed, then defers to the shared modal opener.
- ``webui/static/sync-services.js``: one new branch in
  ``initializeSyncPage`` to lazy-load the tab on first activation.
- ``webui/static/style.css``: ``.listenbrainz-icon`` SVG (orange
  play-button in circle for inactive, white for active),
  ``.listenbrainz-sub-tab-btn`` styling for the sub-tabs,
  ``.refresh-button.listenbrainz`` accent.
- ``webui/static/helper.js``: WHATS_NEW entry under 2.6.3.

Auth-not-connected case is surfaced as a friendly placeholder
pointing the user at Settings → Connections instead of an empty
list.
2026-05-26 14:17:44 -07:00
Broque Thomas
980576f3a8 Sync page: dedicated iTunes Link icon + reorder Qobuz tab
The iTunes Link tab was reusing the generic `import-file-icon` (a
blue document glyph), which read as "import a file" rather than
"iTunes / Apple Music link". Added a dedicated `.itunes-icon`
inline-SVG matching the iTunes 11+ / Apple Music aesthetic —
pink-red circle with a white double-stem note glyph — and switched
the tab button to use it. Stays consistent with the rest of the
tab icons in the file (all inline data URIs, no external fetches).

Also moved the Qobuz tab from between Deezer and Deezer Link to
between Tidal and Deezer, so the Deezer / Deezer Link pair sits
adjacent and the lossless-streaming services (Tidal / Qobuz) group
naturally. Updated the Qobuz Playlist Sync modal-section feature
line to drop the now-stale "between Deezer and Deezer Link"
position claim.
2026-05-26 11:32:38 -07:00
Broque Thomas
718eb0cb10 Add iTunes / Apple Music link import tab on Sync page
New iTunes Link tab between Deezer Link and YouTube. Accepts album,
track, and playlist URLs from music.apple.com / iTunes. Pulls the
tracklist, runs it through the same discovery -> sync -> download
pipeline as the other link tabs.

Apple Music playlists go through amp-api with a Bearer JWT scraped
from the SPA. The legacy meta-tag and inline `"token":"..."` paths
are gone in the current music.apple.com SPA, so the extractor now
walks the page's `<script src>` list (prioritising index/chunk/main
bundles), fetches up to 8 JS bundles, regex-matches JWT-shaped
strings, and base64-decodes each payload to confirm it carries
Apple media-api claims (`root_https_origin`, or `iss + iat + exp`)
before trusting it. Filters out analytics / error-reporter JWTs that
also ship in the bundle.

Tokens are cached at module scope for 6h behind a threading.Lock so
the three-worker discovery executor doesn't thunder-herd Apple on
cold start, and amp-api calls go through a single helper that on
401 invalidates the cache, refetches the page, force-refreshes the
token, and retries the request once. The playlist fetcher memoises
the page HTML for the cache-miss path so we don't refetch it for
every paginated `/tracks` page.

spotify_public discovery worker accepts the new platform shape so
iTunes Link reuses the same matching code path as Deezer Link and
Spotify-public. UI bits live in the sync-services.js iTunes Link
tab, with platform plumbing through wishlist-tools.js for the
multi-source state map.
2026-05-25 22:32:18 -07:00
Broque Thomas
a5c23f898e Quick Actions: soften animations + smooth flow-line reset
Auto-Sync: equalizer cycle slowed 1.6s -> 3.2s, amplitude swing
tightened (0.4-1.0x of base height -> 0.55-0.85x) so the bars
breathe instead of slamming. Playhead duration slowed 5.5s -> 9s
and the line was thinned + given a softer accent color (rgba 0.7
instead of full light) and a smaller drop-shadow. Playhead now
fades in over the first 10% and fades out over the last 15% so it
glides on and off rather than appearing at the edge.

Automations: the flow line was using a background-position sweep
that snapped from end to start each loop — visible as a reset jump
every cycle. Rewrote the sweep as a pseudo-element with its own
translateX + opacity animation: fades in at 15%, runs across, fades
out before snapping back. Node pulse + line sweep both run on the
same 3.2s cycle now so the three nodes and two lines stay in
phase. Node animation delays adjusted to evenly stagger across the
new cycle length.
2026-05-25 16:09:46 -07:00
Broque Thomas
47498b88d3 Quick Actions: fix Automations flow visibility + add hero playhead
Two tweaks based on usage feedback.

Automations flow was anchored at \`right: -8%\` which pushed the
trigger->action->notify chain off the right edge of the minor tile.
Repositioned to fill the bottom of the tile with left/right inset
matching the tile padding, and bumped the base opacity from 0.25 to
0.45 so the chips are actually visible without hovering. Connecting
lines now have a 60%-wide bright accent sweep that travels
left-to-right along each segment in sync with the node pulses, so
the flow reads as a signal propagating through the chain rather
than three nodes blinking in place.

Auto-Sync hero gets a vertical accent playhead that scrolls
left-to-right across the equalizer bars on a 5.5s loop — a
now-playing scrubber overlay that adds horizontal motion to the
existing vertical bar pulse. Drop-shadow filter gives it a soft
glow as it passes over each bar. prefers-reduced-motion disables
both the playhead and the new line sweep.
2026-05-25 16:00:09 -07:00
Broque Thomas
82717dec03 Redesign Quick Actions as asymmetric bento with signature animations
Auto-Sync hero on the left (spans both rows), Tools + Automations
stacked on the right. Each tile gets a CSS-only ambient animation
that visually represents what that section does — no more three
identical rectangles.

Auto-Sync (hero, 2 rows tall): 20-bar live equalizer animates along
the bottom edge with per-bar offsets so it reads as a real audio
waveform. Foreground has a live status pulse dot + accent kicker,
big 56px icon, large title, description, and a CTA bar separated
by a hairline rule.

Tools (top-right): an oversized gear icon rotates slowly off the
right edge as a watermark. Hover speeds it up (28s -> 12s) and
brightens the tint.

Automations (bottom-right): three nodes connected by gradient lines
pulse in sequence, mimicking trigger -> action -> notify flow. Each
node glows + halos on its phase.

Card recipe (gradient body, top accent stripe, accent border on
hover, multi-layer shadow) is the same library-status-card vocab
the rest of the dashboard already uses. Container query
(container-type: inline-size) drives every dimension via
clamp(min, Ncqw + base, max) so padding, text, icon, and animation
sizes scale with the actual card width — no overflow on narrow
dashboards. Single-column stack at <=560px.

prefers-reduced-motion disables all three signature animations.
2026-05-25 15:49:38 -07:00
Broque Thomas
d18f45dfec Auto-Sync: bulk schedule per source + custom interval columns
Two upgrades to the schedule board:

Bulk schedule. Each source group in the sidebar gets a small "Bulk"
button next to the title. Clicking it opens a popover with the same
ten standard buckets plus "Custom interval…" (prompts for hours) and
"Unschedule all". Picking a bucket POSTs/PUTs the schedule for every
schedulable playlist in that source. Result toast aggregates ok/fail
counts. Big quality-of-life for "I want every Spotify playlist
weekly" without 30 individual drags.

Custom interval columns. The board's column set is no longer the
hardcoded `AUTO_SYNC_BUCKETS` list — it's the union of those plus any
hour values currently in use by playlist_schedules. A 6h or 36h
schedule (created via the bulk custom prompt, or hand-edited in the
Automations page) now renders as its own dashed-border column instead
of silently disappearing from the board because it didn't match a
standard bucket. Standard columns still render solid; custom ones get
a "custom" eyebrow + dashed border so they're visually distinct.
2026-05-25 14:54:30 -07:00
Broque Thomas
d668bf4e6d Auto-Sync manager: filter, failure indicators, history filters
Six small UX additions on the Playlist Auto-Sync manager:

- Sidebar gets a "Filter playlists…" search input. Re-renders only
  the schedule panel on input so focus is preserved while typing.
- Scheduled cards show a red `!` badge + red border tint when the
  last three pipeline runs failed (yellow `⚠` if at least one of the
  last few failed). Surfaces chronically broken schedules visually
  instead of leaving them indistinguishable from healthy ones.
- Run History tab title shows a red error count badge when there are
  failed runs in the loaded window.
- Run History tab body gains All / Errors / Completed filter pills
  with per-bucket counts.
- Load-more button at the bottom of the history tab pulls another
  50 entries (capped at 500).
- "Run pipeline again" button in the expanded detail of each history
  card re-triggers that playlist's pipeline directly.

Also dropped the "Discovered: completed" result pill — `tracks_discovered`
in the result payload is a status string, not a count, and the same
data is already in the before/after stats grid above.
2026-05-25 14:22:56 -07:00
Broque Thomas
dfdc6c6277 Restyle Auto-Sync manager and fix loading regressions
Three problems wrapped into one pass on the Playlist Auto-Sync surface:

1. Visual: the manager modal had its own vibe (radial gradient, pill
   tabs, sky-blue chrome) that didn't line up with the rest of the
   app. Reworked the modal shell, KPI summary, live pipeline monitor,
   tab bar, schedule board sidebar, and column cards to use the
   standard SoulSync patterns — gradient `#1a1a1a → #121212`,
   accent-tinted 1px border, 20px radius, underline tabs, dense dark
   card pattern that Automations + Library pages already use. Modal
   now uses near-full screen so there's room for the schedule board
   without horizontal scroll pain. Run history cards followed the
   same path: slim horizontal row mirroring `.automation-card` plus
   an expanded detail that mirrors the Automations run-history modal
   (stats-grid + facts row + result pills + log section).

2. Hang: the previous SQL fix for the run-history "in library" count
   added `COLLATE NOCASE` on the join columns of `tracks` and
   `artists`. SQLite can't use `idx_artists_name` or `idx_tracks_title`
   when the comparison collation doesn't match the column collation,
   so the join did a full table scan per mirrored playlist track.
   ~18s per playlist × 30 playlists = `/api/mirrored-playlists` hung
   indefinitely and the modal stayed at "Loading schedule…" forever.
   Switched the join back to case-sensitive equality (~6ms per
   playlist, 3000× faster). Spotify names canonicalize to the same
   form as library imports so the recall loss is in the rounding
   error of pure case-only mismatches.

3. Slowness: even after the hang fix, each modal open spent ~1.5s
   gathering per-playlist status counts. The endpoint looped
   `get_mirrored_playlist_status_counts(playlist_id)` per row, which
   opened a fresh SQLite connection + PRAGMA setup each time. Added
   `get_all_mirrored_playlist_status_counts(profile_id)` which
   returns counts for every mirrored playlist owned by the active
   profile in 4 batched `GROUP BY` queries over a single connection.
   Modal load dropped to ~280ms.

Also fixed: `tracks.artist` reference in `get_mirrored_playlist_status_counts`
that never worked since the schema went relational — the query threw
"no such column", got swallowed by the try/except, and the in-library
count silently defaulted to 0 on every playlist. Rewired to join
through `artists`.

`get_mirrored_playlist_status_counts` (single-playlist) kept for
callers that still want it, but the modal endpoint uses the batched
version.
2026-05-25 12:24:48 -07:00
Broque Thomas
4aec5584e1 Simplify auto-sync history card layout
Flatten playlist pipeline history cards into a stable header, flow, preview, and detail structure so the run history does not collapse into broken line rows. Keep explicit expand bindings and preserve the richer detail payload rendering.
2026-05-25 08:34:37 -07:00
Broque Thomas
81ad3079b4 Fix auto-sync history card expansion
Bind run history card expand interactions after the modal renders instead of relying on inline handlers, and reshape the run cards into a clearer two-row layout with controlled metadata, preview chips, and roomier expanded detail padding.
2026-05-25 08:28:36 -07:00
Broque Thomas
e86e74d640 Expand auto-sync run history cards
Make playlist pipeline run history cards clickable and keyboard-accessible, with expanded detail sections for summary stats, timeline, before/after snapshots, result payload fields, and future run logs. Refresh the card styling so the expanded state remains responsive inside the Auto-Sync modal.
2026-05-25 08:24:47 -07:00
Broque Thomas
60b346aa33 Standardize auto-sync modal cards
Bring Auto-Sync automation and run-history cards closer to the Automations page pattern with status dots, flow chips, compact metadata, and denser run preview details.
2026-05-25 08:19:05 -07:00
Broque Thomas
119e8f1599 Harden auto-sync history row rendering
Normalize sparse playlist pipeline history rows before rendering and add a visible fallback for empty entries so the Run History tab cannot collapse into unreadable divider lines.
2026-05-25 07:59:27 -07:00
Broque Thomas
06c76bbcaf Make auto-sync run history readable
Render playlist pipeline history as visible run cards with fallback summaries, preview chips, metadata, Details controls, and an explicit empty result message for sparse payloads.
2026-05-25 07:55:52 -07:00
Broque Thomas
9eb7a49c24 Fix auto-sync scheduled card layout
Give placed playlist cards a dedicated content wrapper, full-width action row, wider board columns, and defensive wrapping so titles, timing badges, and Run now controls stay inside card bounds.
2026-05-25 07:46:45 -07:00
Broque Thomas
d8d8e0bcb5 Refine auto-sync modal spacing
Compact the inactive pipeline monitor, widen schedule columns, increase board gutters, and rework scheduled playlist cards so Run now and remove actions no longer crowd playlist text.
2026-05-25 07:44:35 -07:00
Broque Thomas
efdcde1892 Add playlist auto-sync run history
Persist per-playlist pipeline run snapshots from the shared playlist pipeline, expose a history API, and upgrade the Auto-Sync modal with live pipeline monitoring, Run now controls, and a runs-style history tab.
2026-05-25 00:55:55 -07:00
Broque Thomas
f402badac9 Align dashboard actions with accent theme
Update the dashboard Quick Actions tile to use the shared accent color variables for lane glow, icon chips, borders, hover states, and keyboard focus while keeping the three-destination launcher responsive.
2026-05-25 00:38:00 -07:00
Broque Thomas
f67fff22b4 Redesign dashboard quick actions tile
Replace the old Tools CTA with a unified three-lane dashboard launcher for Tools, Auto-Sync, and Automations, using restrained glass/accent styling and responsive stacked behavior.
2026-05-25 00:36:11 -07:00
Broque Thomas
dc4d157944 Fix Auto-Sync next-run countdown and theme its modal
The Playlist Auto-Sync schedule board was showing "next in 8h" on every
card regardless of the configured interval. Root cause: backend stores
next_run as a naive UTC string ("2026-05-25 05:00:00") and the new
auto-sync renderer was parsing it with plain `new Date(...)`, which
treats unmarked timestamps as local time. On Pacific time that offsets
the displayed countdown by ~8 hours. Auto-Sync now routes through the
existing `_autoParseUTC` helper that the rest of the Automations page
already uses, so countdowns line up with the wall clock.

A separate correctness fix in the automation update API: when a PUT
changes `trigger_type` or `trigger_config`, the stored `next_run` is
now blanked before the engine reschedules. Previously the scheduler's
restart-survival path would preserve a stale future timestamp from the
prior interval, so dragging a playlist from the 8h column to the 1h
column kept firing at the old 8h mark. Boot-time restart behavior is
unchanged — only user-driven schedule changes reset the clock.

Modal restyle: the Auto-Sync manager's hardcoded sky-blue palette is
replaced with `var(--accent-rgb)` everywhere so the modal honors the
user's chosen accent color. Tinted glow on the modal border, tabbed
header active state, scheduled-playlist chips, scrollbars, and a new
drag-over highlight on columns all follow the accent theme. The
column drag-over state is wired through new ondragleave handling so
the highlight clears reliably when leaving a column.
2026-05-24 22:01:21 -07:00
Broque Thomas
9a8e7d02a7 Make auto-sync schedule modal responsive
Constrain Auto-Sync columns inside the modal with per-column vertical scrolling, add responsive layouts for narrower and shorter viewports, and separate schedule interval labels from next-run timing.

Also prevents unsupported mirrored sources from being scheduled into the playlist pipeline while still showing them as unavailable in the sidebar.
2026-05-24 21:01:46 -07:00
Broque Thomas
5421f3800e Polish auto-sync manager modal
Upgrade the Auto-Sync modal into a tabbed manager with a richer schedule board and a separate read-only Automation Pipelines tab for existing playlist_pipeline automations.
2026-05-24 20:39:48 -07:00
Broque Thomas
854141f903 Add playlist auto-sync schedule board
Add a Sync-page Auto-Sync manager with source-grouped mirrored playlists, interval columns, and drag/drop scheduling backed by playlist_pipeline automations.

Schedules created by the board are editable there, while existing custom pipeline automations are shown as locked automation-managed entries.
2026-05-24 20:31:03 -07:00
Broque Thomas
46a0999ca2 Clarify mirrored playlist auto-sync action
Rename the manual pipeline button to Auto-Sync and make non-JSON endpoint failures show an actionable restart message instead of a raw JSON parse error.
2026-05-24 20:23:37 -07:00
Broque Thomas
a1409576f4 Move mirrored pipeline action into card controls
Place the Run Pipeline action alongside the existing mirrored playlist card controls so users can find it next to clear, edit source, and delete.
2026-05-24 20:09:06 -07:00
Broque Thomas
f83c671570 Add direct mirrored playlist pipeline runs
Expose playlist-native run and status endpoints that reuse the shared mirrored playlist pipeline engine while routing progress into playlist UI state.

Add a Run Pipeline action to mirrored playlist cards and modals with live status polling, and make the shared pipeline lock atomic for manual and scheduled callers.
2026-05-24 19:54:04 -07:00
Broque Thomas
73bd2db547 Harden playlist pipeline source refresh
Centralize mirrored playlist source reference normalization so edited links and IDs are stored consistently. Preserve URL-backed refresh refs, surface missing-source refresh failures, count background sync failures in pipeline summaries, and retry guarded automation skips after a short delay instead of losing a scheduled run. Add focused coverage for source refs, mirrored playlist source updates, refresh failures, and guarded retry behavior.
2026-05-24 19:31:00 -07:00
Antti Kettunen
7853d0cfb8
chore(webui): remove legacy import feature js / css code 2026-05-24 21:17:19 +03:00
Broque Thomas
a3ba79a9ce Improve radio mode UI and behavior
Refactor and enhance the player radio feature: add npSetRadioMode, npQueueHasNext, and npEnsureCurrentTrackInQueue helpers to centralize radio-state changes and conditional radio fetch logic; replace direct npRadioMode toggles with npSetRadioMode in the expanded player and artist-radio flow (now awaits playLibraryTrack and triggers fetchIfNeeded). Add accessibility (aria-pressed) and label/pulse elements to the radio button, and update CSS for improved visuals and active-state animation. Also adjust toasts/messages and ensure the current library track is seeded into the queue when needed.
2026-05-24 11:02:19 -07:00
Broque Thomas
ccbe918808 Unify artist detail action buttons
Move the artist watchlist and discography actions into the main artist hero action row so they sit with Artist Radio and Enhance Quality. Apply a shared compact pill treatment for the hero actions while preserving the existing button IDs and click behavior.
2026-05-24 10:46:21 -07:00
Broque Thomas
4ca3f70bf3 Show MusicBrainz release variants in import
Expand matched MusicBrainz release groups into concrete releases for specific album searches so import users can choose the correct edition by track count, format, country, and disambiguation. Preserve distinct MusicBrainz release IDs instead of deduping same-title variants, carry release metadata through import matching, and surface those details on album result cards. Add coverage for variant preservation and release-group expansion.
2026-05-24 09:33:19 -07:00
Broque Thomas
a34eae1445 Add Qobuz playlist sync to Sync page (#677)
Qobuz joins Tidal and Deezer as a first-class playlist sync source.
New Qobuz tab on the Sync page lists user playlists + a virtual
Favorite Tracks entry, and clicks route through the same discovery →
sync → download pipeline the other services already use.

Backend:
* core/qobuz_client.py — new get_user_playlists, get_playlist,
  get_user_favorite_tracks, get_user_favorite_tracks_count. Returns
  normalized dicts (matches Deezer client shape, not Tidal's
  dataclasses) so the discovery worker can iterate directly without
  duck-typing. Virtual `qobuz-favorites` ID dispatches to favorites
  fetcher inside get_playlist — same trick Tidal uses with
  COLLECTION_PLAYLIST_ID. Both list endpoints paginate against
  Qobuz's 500-cap limit.
* core/discovery/qobuz.py — new worker module. Mirrors
  core/discovery/deezer.py: pause enrichment, iterate tracks,
  hit discovery cache, fall back to _search_spotify_for_tidal_track,
  build wing-it stub on miss, sync results to mirrored playlist.
* web_server.py — adds /api/qobuz/playlists, /playlist/<id>,
  /discovery/start/<id>, /discovery/status/<id>, /discovery/update_match,
  /playlists/states, /state/<id>, /reset/<id>, /delete/<id>,
  /update_phase/<id>, /sync/start/<id>, /sync/status/<id>,
  /sync/cancel/<id>. One-for-one with the Tidal + Deezer endpoint
  sets. Qobuz discovery executor registered for clean shutdown.

Frontend:
* webui/static/sync-services.js — full handler set (loadQobuzPlaylists,
  createQobuzCard, openQobuzDiscoveryModal, startQobuzDiscoveryPolling,
  startQobuzPlaylistSync, startQobuzSyncPolling, cancelQobuzSync,
  startQobuzDownloadMissing, rehydrateQobuzDownloadModal, etc.).
  Reuses the shared YouTube discovery modal via fake `qobuz_<id>`
  urlHash and is_qobuz_playlist flag. Shared switch statements in
  getModalActionButtons / generateTableRowsFromState / Wing It helpers
  in downloads.js gain new isQobuz branches alongside the existing
  per-service ones.
* webui/index.html — new Qobuz tab button + content div, slotted
  between Deezer and Deezer Link.
* webui/static/style.css — new .qobuz-icon for the tab icon.
* webui/static/core.js — qobuzPlaylists / qobuzPlaylistStates /
  qobuzPlaylistsLoaded globals.

Followed the existing per-service pattern verbatim rather than
refactoring the duplicated transformers across Tidal / Deezer /
Spotify-public / YouTube / Mirrored — that refactor is its own follow-up
PR per the "don't break Tidal/Deezer" scope discipline. Adding the 6th
copy of a proven pattern is lower risk than collapsing 5 working
services behind a new abstraction.

Tests:
* tests/test_qobuz_playlists.py — 12 tests covering pagination,
  normalization, favorites virtual-ID routing, artist-name fallback
  chain (performer → album.artist → 'Unknown Artist'), and
  unauthenticated short-circuits.
2026-05-23 23:27:36 -07:00
Broque Thomas
eba7f61e04 Surface metadata source on Import album results (#681)
Import album search silently fell through to the next source in
METADATA_SOURCE_PRIORITY when the configured primary returned zero
matches — intentional behavior shared with the auto-import worker
(see core/auto_import_worker.py:1316). With MusicBrainz selected and
a query MB couldn't resolve, users saw Deezer cards with no indication
their primary was bypassed.

Backend now echoes `primary_source` on /api/import/search/albums,
/api/import/search/tracks, and /api/import/staging/suggestions.
Frontend renders a per-card 'via {source}' badge when the served
source differs from the primary, plus a banner above the grid when
every card came from a fallback source. Fallback semantics unchanged.

Also collapses an inline duplicate of _renderSuggestionCard inside
importPageSearchAlbum into a single shared renderer.

Regression test pins the contract: response carries primary_source +
per-album source when the chain falls back.
2026-05-23 16:22:17 -07:00
Broque Thomas
94129d3099 Clarify hybrid source album behavior
Add dynamic level badges to the hybrid source order settings. The first enabled source shows Album-level only when it supports album-bundle downloads; every other source shows Track-level to make fallback behavior visible.

Update the helper copy and badge styling so users can understand why putting Soulseek, Torrent, or Usenet first changes album-download behavior.
2026-05-23 15:15:28 -07:00
Antti Kettunen
5b82e6c1ba
refactor(webui): remove legacy stats page assets
- delete the old stats page HTML, JS, and CSS now that the React route owns the experience
- preserve helper/tour selectors by exposing the legacy stats ids from the React page
- move shared track playback fallback into library code
2026-05-23 21:22:45 +03:00