From 79465580ab47aadddd79864eb44aac0cf8deff10 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 27 May 2026 19:22:28 -0700 Subject: [PATCH] Dashboard: equalizer bars now show real brand logos in the avatar disc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ```` 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 - ```` 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. --- webui/static/api-monitor.js | 41 ++++++++++++++++------ webui/static/audiodb.png | Bin 0 -> 29987 bytes webui/static/helper.js | 2 +- webui/static/style.css | 68 ++++++++++++++++++++++++++---------- 4 files changed, 82 insertions(+), 29 deletions(-) create mode 100644 webui/static/audiodb.png diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js index f2b07ac3..692249ca 100644 --- a/webui/static/api-monitor.js +++ b/webui/static/api-monitor.js @@ -19,13 +19,23 @@ const _RATE_GAUGE_COLORS = { amazon: '#FF9900', }; -// 2–3 character abbreviations rendered inside each equalizer bar's -// avatar disc. First-letter alone collided too often (3× "A" services, -// 2× "D"); these read as service shorthand at a glance. -const _RATE_GAUGE_GLYPHS = { - spotify: 'SP', itunes: 'AM', deezer: 'DZ', lastfm: 'LF', genius: 'GN', - musicbrainz: 'MB', audiodb: 'ADB', tidal: 'TD', qobuz: 'QB', discogs: 'DC', - amazon: 'AZ', +// Brand logos rendered inside each equalizer bar's avatar disc. +// Same URLs the header-actions worker-orb buttons use — sourced +// from each service's official press / SVG-repo asset so the row +// reads as branded chips, not anonymous initials. AudioDB ships +// no public logo URL, so it lives as a local static file. +const _RATE_GAUGE_LOGOS = { + spotify: 'https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png', + itunes: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/ITunes_logo.svg/960px-ITunes_logo.svg.png', + deezer: 'https://cdn.brandfetch.io/idEUKgCNtu/theme/dark/symbol.svg?c=1bxid64Mup7aczewSAYMX&t=1758260798610', + lastfm: 'https://www.last.fm/static/images/lastfm_avatar_twitter.52a5d69a85ac.png', + genius: 'https://images.genius.com/8ed669cadd956443e29c70361ec4f372.1000x1000x1.png', + musicbrainz:'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/MusicBrainz_Logo_%282016%29.svg/500px-MusicBrainz_Logo_%282016%29.svg.png', + audiodb: '/static/audiodb.png', + tidal: 'https://www.svgrepo.com/show/519734/tidal.svg', + qobuz: 'https://www.svgrepo.com/show/504778/qobuz.svg', + discogs: 'https://www.svgrepo.com/show/305957/discogs.svg', + amazon: '/static/amazon.svg', }; // Per-service display state for the equalizer bars. Holds the last @@ -185,7 +195,8 @@ function _renderEqualizerBars(grid, data) { for (const svc of _RATE_GAUGE_SERVICES) { const accent = _RATE_GAUGE_COLORS[svc] || '#888'; const label = _RATE_GAUGE_LABELS[svc] || svc; - const glyph = _RATE_GAUGE_GLYPHS[svc] || (label[0] || '?').toUpperCase(); + const logoSrc = _RATE_GAUGE_LOGOS[svc] || ''; + const fallbackGlyph = (label[0] || '?').toUpperCase(); const bar = document.createElement('button'); bar.type = 'button'; bar.className = 'rate-eq'; @@ -194,12 +205,22 @@ function _renderEqualizerBars(grid, data) { bar.setAttribute('aria-label', `${label} rate detail`); bar.onclick = () => _openRateModal(svc); // Layout, top-to-bottom: - // - avatar disc (brand glyph) anchored above the track + // - avatar disc (brand logo) anchored above the track — + // ``onerror`` swap to the initial-letter glyph keeps the + // UI legible if a CDN URL ever breaks // - track (with reflection puddle as ::after) holding the // fill / shimmer / peak tip / live count // - meta column (state pill + service name) + const avatar = logoSrc + ? `` + : ``; bar.innerHTML = ` - + ${avatar}
diff --git a/webui/static/audiodb.png b/webui/static/audiodb.png new file mode 100644 index 0000000000000000000000000000000000000000..fefba792daedbf1f42a9dc18e690f2810b276a4c GIT binary patch literal 29987 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 literal 0 HcmV?d00001 diff --git a/webui/static/helper.js b/webui/static/helper.js index f63df992..4f2f7292 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3415,7 +3415,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.6.3': [ { unreleased: true }, - { title: 'Dashboard: enrichment equalizer bars get next-level polish', desc: 'follow-up to the equalizer redesign. four upgrades that take the bars from "nice" to "vibey": (1) every bar now carries a circular brand-color avatar disc above the track with the service\'s 2-3 letter glyph (SP, AM, DZ, LF, GN, MB, ADB, TD, QB, DC, AZ) — disc has its own radial gradient + inner highlight + slow halo pulse when the worker is running, ties the bar visually to its identity; (2) peak-flash detector — when the worker\'s calls/min actually steps upward between updates, the peak tip briefly flares white-hot and the fill flashes brighter for ~650ms, like a hardware VU meter\'s peak-detect LED catching a beat. only fires on real increases above a noise threshold so jitter doesn\'t pulse the bar constantly; (3) rolling number counter — the live count digit-animates from old→new value with easeOutCubic instead of snapping, the same premium-counter feel you\'d see on a fancy dashboard; (4) glass-surface reflection puddle under each bar — soft accent-colored glow whose opacity tracks the real (unclamped) rate, so the row looks like the bars are standing on a polished pane rather than floating in space. puddle ripples on peak-flash too. all four tied together by the same --eq-accent / --eq-glow CSS variables so future tweaks stay coherent.', page: 'dashboard' }, + { title: 'Dashboard: enrichment equalizer bars get next-level polish', desc: 'follow-up to the equalizer redesign. four upgrades that take the bars from "nice" to "vibey": (1) every bar now carries a circular avatar disc above the track loading the service\'s real brand logo (the same images the header-action worker orbs use — Spotify, Apple Music, Deezer, Last.fm, Genius, MusicBrainz, AudioDB, Tidal, Qobuz, Discogs, Amazon Music). Disc has a dark glass backdrop, accent-tinted ring, and slow halo pulse when the worker is running; the brand color shows through as a drop-shadow on the logo + ring tint. Initial-letter fallback kicks in automatically if any CDN URL fails; (2) peak-flash detector — when the worker\'s calls/min actually steps upward between updates, the peak tip briefly flares white-hot and the fill flashes brighter for ~650ms, like a hardware VU meter\'s peak-detect LED catching a beat. only fires on real increases above a noise threshold so jitter doesn\'t pulse the bar constantly; (3) rolling number counter — the live count digit-animates from old→new value with easeOutCubic instead of snapping, the same premium-counter feel you\'d see on a fancy dashboard; (4) glass-surface reflection puddle under each bar — soft accent-colored glow whose opacity tracks the real (unclamped) rate, so the row looks like the bars are standing on a polished pane rather than floating in space. puddle ripples on peak-flash too. all four tied together by the same --eq-accent / --eq-glow CSS variables so future tweaks stay coherent.', page: 'dashboard' }, { title: 'Dashboard: equalizer-bar redesign for the Enrichment Services panel', desc: 'the enrichment rate monitor on the dashboard used a 10-column grid of circular speedometers, which (a) lost symmetry the moment a new service was added — 11 services meant 10 in one row and a lonely orphan in the next, (b) wasted ~80% of pixels on empty gauge arcs showing "0" most of the time. Replaced with a VU-meter / equalizer-bar row: each service is a vertical capsule with the service\'s brand-color fill height tracking calls/min, a continuous shimmer scan + glowing peak tip when active, a slow breathing animation when idle, and a red pulsing variant when rate-limited. Symmetric by design at any service count — the row uses flex with even gaps so new services just slot in. Bar tip + value text adapt color to the service\'s accent. Status pill below each bar (Running / Idle / Paused / Stopped) with its own pulsing dot. Click any bar to open the same detail modal the speedometer used; nothing changed about the underlying data flow.', page: 'dashboard' }, { title: 'Auto-Sync: fix ListenBrainz pipelines stuck on "Refreshing:" for 5+ minutes', desc: 'Auto-Sync pipeline runs against a ListenBrainz playlist (Weekly Jams / Weekly Exploration / Top Discoveries / etc.) would sit on "Refreshing: \'\'" for minutes with zero UI updates before anything else happened. Two real bugs ganging up: (1) the refresh path ran the LB matching engine on every track to produce matched_data — 5+ minutes blocking with no progress emission — and Phase 2 of the pipeline then ran the SAME matching engine on the same tracks via the discovery worker. So LB tracks discovered twice, the first time silently. (2) ListenBrainz manager only exposed an `update_all_playlists` entry-point — refreshing one playlist re-pulled all 12+ cached LB playlists\' details from the API even though we only cared about one. Pipeline now skips the refresh-side discovery (Phase 2 handles it with progress emits every 3s — same pattern that already works for Spotify); refresh uses a new targeted `LBManager.refresh_playlist(mbid)` that hits just the requested playlist. Also killed a silent `except Exception: pass` in the LB adapter that was masking real API failures as stale-cache hits — refresh errors now log with traceback and surface to the run-history error tally. Pinned with 12 new unit tests.', page: 'automations' }, { title: 'Wishlist: harden Spotify backfill so a poisoned track_number can\'t mask a lean album', desc: 'follow-up to the earlier wishlist import-path work. residual per-track wishlist downloads (single tracks from different albums, falling below the album-bundle threshold) were producing folders without a year subfolder whenever the underlying wishlist row had a track_number=1 from an older default — the candidate dispatcher\'s "fall back to Spotify API" branch was gated entirely on track_number being missing, but the same API call was also the only thing that hydrated the lean album_context (release_date / total_tracks / cover art) when the original discovery match came from Deezer\'s search endpoint which doesn\'t carry those fields. so any row whose track_number happened to look "filled" (1 from a default) would short-circuit the API call and the year disappeared from the folder path even though the API knew it. split the two concerns: track_number resolution keeps its track_info → track object → API precedence, but album hydration now runs whenever release_date or total_tracks are missing regardless of where track_number came from. one network round-trip still serves both. lifted into core/downloads/track_metadata_backfill.py with 24 unit tests pinning every branch — including the regression: poisoned default-1 track_number does NOT block album backfill anymore.' }, diff --git a/webui/static/style.css b/webui/static/style.css index a8af4bbc..518fd0b3 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -62289,34 +62289,64 @@ body.reduce-effects .dash-card::after { transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); } -/* Avatar disc — circular brand-color chip above the bar, glyph - * centered. Anchors each capsule to its service so the row reads - * as identifiable rather than 11 anonymous bars. */ +/* Avatar disc — circular chip above the bar carrying the service's + * real brand logo. Dark glass backdrop with a thin accent ring + + * accent-colored drop-shadow halo so the logo gets its breathing + * room without losing the per-service color cue. ``--fallback`` + * variant kicks in via the ```` onerror handler when a CDN URL + * breaks, restoring the accent-gradient + initial-letter look. */ .rate-eq-avatar { - width: 30px; - height: 30px; + width: 34px; + height: 34px; border-radius: 50%; margin: 0 auto; display: flex; align-items: center; justify-content: center; - font-size: 11px; - font-weight: 800; - letter-spacing: 0.04em; - color: rgba(255, 255, 255, 0.95); + background: + radial-gradient(circle at 30% 25%, + rgba(255, 255, 255, 0.08), + rgba(0, 0, 0, 0.35) 70%); + border: 1px solid color-mix(in srgb, var(--eq-accent) 50%, rgba(255, 255, 255, 0.12)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 4px 14px color-mix(in srgb, var(--eq-accent) 30%, transparent), + 0 0 0 0 color-mix(in srgb, var(--eq-accent) 35%, transparent); + transition: box-shadow 0.3s, transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + flex: 0 0 auto; + overflow: hidden; + position: relative; +} + +.rate-eq-avatar-logo { + width: 75%; + height: 75%; + object-fit: contain; + object-position: center; + display: block; + /* The brand-color drop-shadow pops the logo against the dark + * disc so even dark / multi-tone logos retain the per-service + * cue without washing out. */ + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55)) + drop-shadow(0 0 4px color-mix(in srgb, var(--eq-accent) 35%, transparent)); +} + +/* Fallback variant — restores the original accent-gradient disc + + * initial-letter glyph when the brand logo URL fails to load. */ +.rate-eq-avatar--fallback { background: radial-gradient(circle at 30% 25%, color-mix(in srgb, var(--eq-accent) 65%, white 35%), var(--eq-accent) 55%, color-mix(in srgb, var(--eq-accent) 55%, black 45%) 100%); - border: 1px solid color-mix(in srgb, var(--eq-accent) 55%, transparent); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.25), - 0 4px 14px color-mix(in srgb, var(--eq-accent) 30%, transparent), - 0 0 0 0 color-mix(in srgb, var(--eq-accent) 35%, transparent); +} + +.rate-eq-avatar-glyph { + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + color: rgba(255, 255, 255, 0.95); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); - transition: box-shadow 0.3s, transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); - flex: 0 0 auto; } .rate-eq.active .rate-eq-avatar { @@ -62733,7 +62763,8 @@ body.reduce-effects .dash-card::after { .rate-eq-value { font-size: 16px; } .rate-eq-name { font-size: 10px; } .rate-eq-state { font-size: 8px; padding: 1px 6px; } - .rate-eq-avatar { width: 26px; height: 26px; font-size: 10px; } + .rate-eq-avatar { width: 28px; height: 28px; } + .rate-eq-avatar-glyph { font-size: 10px; } } @media (max-width: 720px) { @@ -62750,7 +62781,8 @@ body.reduce-effects .dash-card::after { .dash-card[data-card="enrichment"] .rate-monitor-grid--equalizer { --eq-track-h: 108px; } - .rate-eq-avatar { width: 24px; height: 24px; font-size: 9px; } + .rate-eq-avatar { width: 26px; height: 26px; } + .rate-eq-avatar-glyph { font-size: 9px; } } /* Active downloads container */