Commit graph

585 commits

Author SHA1 Message Date
BoulderBadgeDad
ffbe669c67 Now Playing: vibrant album-art color extraction + drag-to-reorder queue
Two next-level player features (frontend-only):

1. Album-art ambient color — replaced the flat pixel AVERAGE (which muddied
   every cover to grey-brown) with dominant-VIBRANT extraction: coarse
   histogram binning weighted by saturation² × population, then a punch-up
   pass (boost saturation ~1.3x, floor brightness) so the modal glow reads as
   the cover's real standout color, Apple-Music style. Feeds the existing
   --np-ambient-r/g/b hooks.

2. Drag-to-reorder queue — queue rows are now draggable; npReorderQueue moves
   the item AND recomputes npQueueIndex so the currently-playing track stays
   correctly tracked after a reorder. Accent drop-line indicator, grab cursor,
   dragging opacity.

Verified live in-browser by Boulder.
2026-05-30 11:46:31 -07:00
BoulderBadgeDad
3461d9235b Now Playing modal: full visual redesign + click-art visualizer, sleep timer, up-next
Player-revamp frontend (Phase 1). Brings the Now Playing modal to the approved
mockup look + features:

- Full restyle (override block in style.css): 28px modal radius, stronger
  art-driven ambient glow, 340px rounded art that scales while playing, bold
  28px title, accent artist name, accent FLAC pill, dominant 70px gradient
  play button, accent-gradient progress/volume/visualizer. All driven by the
  existing --accent-rgb / --accent-light-rgb so it follows the settings accent.
- Click album art -> Plexamp-style visualizer takeover, fed by the REAL
  music-synced Web Audio analyser (npStartVisualizerLoop), click again -> art.
- Rich queue rows: album thumbnail + title/artist + duration, equalizer
  animation on the now-playing row, hover-reveal remove.
- Up-next peek below the controls (shows the next queued track).
- Sleep timer (cycles 15/30/60m, real setTimeout -> handleStop).
- Crossfade toggle present (visual state + persisted pref; the dual-audio
  crossfade engine is the next step, not yet wired).

Frontend-only; verified live in-browser by Boulder. No backend/test surface.
2026-05-30 11:43:45 -07:00
BoulderBadgeDad
9b34d06b6d UI: migrate remaining compact button families to the .btn--sm tier
Continue the design-system unification (kettui UI-consistency item):
migrate the five remaining compact button families onto the shared
.btn .btn--sm primitive + color modifiers, and drop their bespoke base
CSS (net -125 lines of CSS).

- ya-header-btn (Your Albums/Artists, discover.js-injected) -> .btn .btn--sm
  .btn--secondary; ya-refresh/ya-settings/ya-viewall co-modifiers kept.
- explorer-action-btn (Playlist Explorer) -> .btn--secondary / .btn--primary.
- repair-bulk-btn -> .btn--secondary / .btn--primary / .btn--warning (fix-all).
- enhanced-bulk-btn (Library bulk bar, library.js-injected) -> .btn--primary/
  --secondary/--danger; class kept as a hook for the mobile.css size
  override + the .tag-write / .rg-analyze special colors.
- profile-create-btn (init.js-injected) -> .btn .btn--block .btn--primary;
  class kept for the scoped .profile-edit-buttons flex:1 rule.

mini-nav-btn deliberately left as a distinct icon-button archetype.
2026-05-29 11:40:31 -07:00
BoulderBadgeDad
169c30fd5b UI: add .btn--sm/.btn--block/.btn--warning tier; migrate sync-history buttons
Formalize the compact 'toolbar' button tier as design-system modifiers
(.btn--sm), plus a full-width (.btn--block) and amber caution
(.btn--warning) modifier, so the many smaller per-page buttons can share
the .btn primitive without being forced to the large default size.

First adopter: the Sync page header buttons (.sync-history-btn) now use
.btn .btn--sm .btn--secondary. The class is kept as a JS/onboarding
selector hook; .auto-sync-manager-btn still tints Auto-Sync accent.
2026-05-29 11:30:13 -07:00
BoulderBadgeDad
ae0968e1b0 UI: migrate watchlist/wishlist action buttons to the shared .btn primitive
The watchlist + wishlist header/overview buttons used a bespoke
.watchlist-action-btn family (different padding/radius/font and white
primary text) instead of the shared .btn design-system primitive.
Migrate all 11 of them to .btn / .btn--primary / .btn--secondary /
.btn--danger so they match the rest of the app, and drop the now-dead
CSS.

The .watchlist-batch-remove-btn / .wishlist-batch-remove-btn hook
classes are kept on the remove buttons (their !important red overrides
compose correctly over .btn--secondary). Static HTML only; no JS-injected
usages, and mobile.css overrides target .playlist-modal-btn, not these.
2026-05-29 11:18:19 -07:00
BoulderBadgeDad
6129ea8508 UI consistency: normalize exception-page outer gutter to .page (40px)
Low-risk tidy-up for the full-bleed "exception" pages that aren't carded.
Every page already gets a 40px gutter from .page, but the exception pages were
piling on inconsistent extra padding (library +20px, active-downloads +28/32px,
discover/docs +0) — giving accidental 60 / 68-72 / 40 effective gutters.

Drop the redundant container padding on library and active-downloads so the
single .page 40px gutter is the shared, intentional outer spacing across the
full-width exception pages. discover (centered max-width) and docs (sidebar
layout) keep their functional layout; library's mobile padding override is
unaffected.
2026-05-29 08:53:03 -07:00
BoulderBadgeDad
079c169f8d UI consistency (tabs/cards): add .tab and .card primitives (no migration)
Add the canonical .tab (bordered rounded-pill filter tab) and .card ("glass"
content card) primitives as the documented design-system standard for new
markup and the React pages — modeled on the cleanest existing looks
(watchlist filter pill; dashboard service/stat card).

Deliberately NOT migrating the existing tab/card components onto them: the
current implementations are visually divergent and JS-coupled (active-state
toggled by class name, cards built in JS), so a blind consolidation risks
subtle regressions. These primitives let new/React code be consistent now;
the legacy components migrate when visually verifiable / in React.

Unused classes -> zero visual change to the current UI.
2026-05-29 08:28:18 -07:00
BoulderBadgeDad
dd5fe844d4 UI consistency (buttons 2/N): wishlist modal buttons -> .btn
Migrate the wishlist add-to-wishlist modal buttons onto the shared .btn
primitive: primary -> .btn--primary, secondary -> .btn--secondary, the green
download CTA -> new .btn--download modifier. Added a shared .btn.loading state
(amber pulse, reusing the existing pulse-loading keyframe) since
confirm-add-to-wishlist-btn toggles `loading` via JS (wishlist-tools.js).

Removed the dead .wishlist-modal-btn* rules and re-scoped the mobile
full-width override to `.wishlist-modal-actions .btn`.
2026-05-28 23:48:23 -07:00
BoulderBadgeDad
eebc58d3ff UI consistency (buttons 1/N): add shared .btn primitive; migrate config-modal
Start of the button-consolidation pass (kettui's #1). The app had ~236 button
classes / ~8-10 distinct looks with heavy near-duplication.

Introduce a canonical .btn design-system primitive (base + .btn--primary /
.btn--secondary / .btn--danger), modeled on the dominant existing look
(accent-gradient primary, translucent ghost, semantic danger) and built on the
accent CSS vars. New markup and the React pages should use this; existing
per-page button classes will migrate onto it family by family.

First family migrated: the config/settings modal buttons (.config-modal-btn*,
4 static uses, no JS refs) -> .btn .btn--primary / .btn--secondary. Removed the
now-dead .config-modal-btn* rules and re-scoped its mobile full-width override
to `.config-modal-actions .btn`.

Visible change is minor by design (padding 28->24px, gradient direction
normalized). Proof step for sign-off on the .btn look before rolling wider.
2026-05-28 23:40:10 -07:00
BoulderBadgeDad
45bbc99d94 UI consistency (page shell 3/N): playlist-explorer adopts .page-shell card
Convert the playlist-explorer page from a flat padded container to the
.page-shell floating card. Drop its bespoke `padding: 24px 32px`; keep the
full-height flex layout (display:flex / column / min-height:100%) since the
explorer fills the viewport.

Visible change by design. Watch: the full-height min-height:100% inside a
margin:20px card may run slightly tall — to be confirmed visually.
2026-05-28 22:57:09 -07:00
BoulderBadgeDad
def58a9907 UI consistency (page shell 2/N): automations adopts .page-shell card
First of the "flat -> card" conversions. The automations list view sat
directly on the page background (.automations-container = bare padding) while
its inner .dashboard-header is the same header dashboard uses. Adopt
.page-shell so the page becomes a floating gradient card structurally
identical to the dashboard (page-shell card > dashboard-header > content).

- Drop .automations-container's bespoke `padding: 20px 24px` (card padding now
  from .page-shell); keep the class as the mobile/JS hook.
- Add `page-shell` to the container in markup.

Visible change by design (this page was not previously a card). Mobile keeps
its existing .automations-container padding override.
2026-05-28 22:54:31 -07:00
BoulderBadgeDad
d2a730a6aa UI consistency (page shell 1/2): extract shared .page-shell primitive
First step of the page-layout-shell standardization (kettui's UI-consistency
point #1). The dashboard, tools, watchlist and wishlist pages each defined a
byte-identical "card" container (padding 28px 24px 30px, margin 20px, gradient
bg, radius 24px, border + border-top, layered shadow) under four different
class names.

Extract that into a single `.page-shell` primitive (modeled on the canonical
dashboard/stats look) and have the four pages adopt it. Each keeps its bespoke
class for page-specific extras and as a JS/mobile hook:
- dashboard-container: keeps display:flex / column / gap:25px
- watchlist/wishlist-page-container: keep position:relative
- tools-page-container: no extras (box now fully from .page-shell)

Zero visual change: computed styles are identical (declarations relocated, not
altered), and mobile.css overrides still target the retained bespoke classes.
Per-page themed headers (watchlist amber, etc.) are intentionally NOT touched.

The class name is intended for reuse by the React pages too, so the primitive
is shared across both stacks.

Next (wave 2): migrate settings / automations / playlist-explorer / library
onto .page-shell, which snaps their slightly-off spacing to canonical.
2026-05-28 22:38:55 -07:00
BoulderBadgeDad
ff974c0b5c Standardize artist-detail hero action buttons
The four artist-hero buttons (Artist Radio, Watchlist, Download
Discography, Enhance Quality) had drifted apart visually — different
sizes, weights, radii, hover treatments, and ad-hoc colors. Unify them
on the Download Discography look (the nicest of the set): accent-style
gradient fill, matching border, light-tinted text, compact 7x16 / 12px /
700 sizing, and a translateY(-1px) + colored glow on hover.

To keep them distinguishable, each carries its own hue within that
shared recipe:
- Artist Radio       -> violet (#8b5cf6)
- Watchlist          -> theme accent (keeps its amber "watching" state)
- Download Discography -> theme accent + shimmer (the primary action)
- Enhance Quality    -> cyan (#4fc3f7, its original signature color)

Also:
- Drop the shimmer from the three secondary buttons — four simultaneous
  shimmers were distracting; it now marks only the primary action.
- Remove the `margin: 12px 0 4px` on `.discog-download-wrap` (now
  `margin: 0; display: inline-flex`) that pushed the discography button
  ~4px below its siblings in the centered flex row.
- Include Artist Radio in the mobile button sizing override (was missing).
2026-05-28 12:35:02 -07:00
Broque Thomas
7145368d42 Basic search: visual overhaul + per-source picker in hybrid mode
Two things in this commit. Functional download / matched-download
behaviour is untouched — same JS handlers, same routes for the
download actions, same album-expand interaction.

VISUAL REDESIGN
- Glass search-bar card with accent radial wash + focus ring + pill
  primary search button
- Source chip row above the search bar (see below)
- Always-visible compact filter pill row (Type / Format / Sort) —
  pills carry both ``bs-filter-pill`` (new visual) and ``filter-btn``
  (legacy class for ``resetFilters`` + ``applyFiltersAndSort`` in
  wishlist-tools.js to keep working)
- Accent-tinted status pill matching the dashboard / auto-sync look
- Album result cards: glass card with accent left-edge stripe,
  52px brand-tinted cover icon, chevron expand indicator, pill
  action buttons (Download / Matched Album), accent glow on hover
- Track result cards: glass row with accent stripe, 44px icon,
  pill action buttons (Stream / Download / Matched Download)
- Multi-disc separators inside expanded album track lists styled
  with the accent treatment
- Responsive: action button columns stack vertically below 900px

New CSS lives in a self-contained ``webui/static/basic-search-v2.css``
sheet linked from index.html. Selectors are scoped to
``#basic-search-section`` for any class that already exists in
style.css (``.album-result-card``, ``.album-icon``, ``.track-*``,
etc.); the new ``bs-*`` prefixed classes for the search bar /
filters / source row / status are unscoped because they only exist
in the new markup. ``!important`` is used on the card-level rules
to defeat the original unscoped ``.album-result-card`` etc. rules
in style.css that would otherwise leak heavyweight padding /
box-shadow / 56px icon styles into the new design.

Also removed ``overflow: hidden`` from the original
``.album-result-card`` and ``.track-result-card`` rules in style.css
— those two classes only render in ``downloads.js`` basic search
results (verified via grep, two render sites only), so the
removal can't impact any other UI.

SOURCE PICKER (hybrid mode)
- New ``GET /api/search/sources`` endpoint returns the list of
  active sources from the orchestrator's chain (or the single
  active source in single-source mode).
- Frontend renders a chip row above the search bar. Click a chip
  to target that source for the next search; the chip's brand
  accent fills.
- In single-source mode the lone chip is rendered as a dashed-
  border label so the user always knows what they're searching
  but can't accidentally try to switch to sources that aren't
  configured.
- ``/api/search`` accepts an optional ``source`` body param. When
  set, ``core/search/basic.py:run_basic_search`` resolves the
  client directly via ``orchestrator.client(source)`` and calls
  its ``.search()`` instead of going through the hybrid chain.
- Backwards compatible: omitting ``source`` falls through to the
  original ``orchestrator.search()`` call exactly as before.
  Unknown source names also fall back to the default — typo
  protection.

TESTS (5 new + 6 pre-existing = 11 total in test_search_basic.py)
- source param routes to specific client, NOT orchestrator chain
- no source param preserves original orchestrator-default behaviour
- unknown source name falls back to orchestrator default
- ``run_basic_soulseek_search`` backwards-compat alias preserved
- source-targeted path serialises albums + tracks correctly

101 search-suite tests pass.
2026-05-28 10:22:07 -07:00
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