Merge album/FTS/track sources for title search (any-word LIKE + prefix FTS),
dedupe cluster text hits by album id instead of album_key, route scoped
cluster browse through per-server advanced search, and prefer the local
index over network race when the library index is enabled.
When two or more sidebar libraries are selected in cluster mode, All Albums
text search uses the merged local index only so an empty search3 response from
one member cannot win the local-vs-network race.
Add library_list_albums (album table when unscoped, track GROUP BY when
scoped), cluster single-member bypass, ready-member caching, and UI load
deferrals. Remove REST scope allowlists from local hot paths; fix multi-
library scope union and invalidate cluster scope cache on picker toggles.
Apply cached getAlbumList2 allowlists per server in SQL and post-filter
so narrowed sidebar scope does not leak albums from other libraries or
cluster members. Wire restrictAlbumScopes into cluster advanced search
and dedicated lossless browse; detect cluster scope in All Albums paging.
Introduce albumBrowseExecution with explicit filter layers (library scope,
AND attributes, OR genre union, finalize) so multi-genre and cluster paths
cannot leak albums from other libraries. Wire multi-select sidebar picker
scope through local index, cluster merge reads, and REST allowlist fallback.
Cluster mode lists music folders from every member (cached for instant
picker), applies per-server musicLibraryFilter scopes in merged browse,
and passes libraryScopes through Rust list/advanced-search SQL filters.
Dropdown width sizes to content; single-server picker unchanged.
Resolve preview container hints from HTTP headers, Subsonic suffix, and
magic-byte sniff after Symphonia 0.6. Route preview streams through
clusterBrowseServerId like main playback; guard CoverArtImage when the
preview cover ref is still loading.
Load artist album tracks via getAlbumForServer when clusterSeedServerId is set
so Play All, shuffle, and top-track clicks resolve on the correct member server.
Also improve connection indicator tooltips, Song Info server labels, artist browse
covers/detail fallbacks, and faster merged artist list SQL.
Add Rust library_cluster_album_detail and library_cluster_artist_detail
with cluster_key dedup, owner metadata fallback, and merged tracklists.
Wire cluster-mode hooks and navigation seedServerId so detail routes
never fall back to single-server getAlbum/getArtist.
Resolve tracks to cluster candidates at play time with cascade fallback;
fan-out scrobble/star/rating to all members; block Orbit host in cluster
mode; warn on mixed-server queue share; serve random picks from merged index.
Add merged favorites listing (starred on any member) and cluster-scoped
play_session stats with cluster_key dedup; wire Favorites page and player
statistics panel to cluster APIs when a cluster is active.
Add Rust list_merged_albums/artists commands and wire cluster-mode reads
into tracks, albums, artists catalog, and text search browse paths via
clusterBrowse helpers.
Add cluster management in Settings (create, reorder, rename, sync toggle),
connection indicator dropdown for servers vs clusters, delete guard when a
server is in a cluster, and i18n across all locales.
Add persisted clusters/activeClusterId, cluster CRUD actions, scope helpers,
representative recompute hook, library_cluster_* API wrappers, and vitest
coverage for cluster store rules and delete guard.
* feat(audio): migrate Symphonia 0.5 -> 0.6
Port the audio + analysis pipeline to the Symphonia 0.6 API:
- bump symphonia to 0.6 and symphonia-adapter-libopus to 0.3; drop rodio's
symphonia-all feature to avoid a duplicate symphonia-core 0.5
- remove the local symphonia-format-isomp4 0.5 patch and rely on upstream 0.6
- switch codec registries to register_enabled_codecs / make_audio_decoder with
AudioCodecParameters + AudioDecoderOptions
- rework decode.rs SizedDecoder and analysis decode loops for the new
AudioDecoder trait, GenericAudioBufferRef, Time/Timestamp newtypes, and
next_packet() -> Result<Option<Packet>>
Streaming regression fixes:
- ProbeSeekGate hides seekability during probe for non-MP4 progressive streams
so Symphonia 0.6's trailing-metadata scan no longer forces a full download
before ranged FLAC/MP3/OGG playback can start
- guard the streaming probe() with a 20s timeout on a worker thread so a stalled
ranged source (e.g. right after a server switch) can no longer hang playback
start until a player restart; add probe start/done diagnostics
* docs(changelog): note Symphonia 0.6 migration and streaming fixes (#999)
Add CHANGELOG entries (Changed + Fixed) and a CONTRIBUTORS line for the
Symphonia 0.6 migration, ranged-stream start-latency fix, and probe timeout.
* test(audio): cover streaming probe path and ProbeSeekGate
Add unit tests for SizedDecoder::new_streaming (success + garbage) and
ProbeSeekGate (seekability toggle, byte_len, read/seek passthrough) to
restore decode.rs above the 70% hot-path coverage gate (67.3% -> 79.4%).
* feat(sidebar): add toggle to pin Now Playing to the top
The fixed Now Playing entry can now be pinned to the very top of the
sidebar (above the Library label) instead of sitting above the bottom
spacer. Adds a persisted `nowPlayingAtTop` setting (default off, so
existing layouts are unchanged) and a toggle in the sidebar customizer
next to the Mix-navigation split. The entry stays non-hideable and
keeps its playing indicator in both positions.
* i18n(settings): Now Playing-at-top toggle strings (9 locales)
* docs(changelog): note Now Playing top toggle (#1000)
* fix(settings): restore full border on active server card
The active server card mixed the `border` shorthand with borderTop/
borderBottom longhands in one inline style object. React clears the
unset longhand sides on mount, so the top and bottom borders fell back
to the subtle base border while only the left/right accent border
showed. Drive the drag drop-target indicator via an inset box-shadow
instead, leaving the border shorthand to apply on all four sides.
* docs(changelog): note active server card border fix (#998)
* chore: restore PR order in CHANGELOG [1.47.0] Fixed section
Re-sort ### blocks ascending by PR number per release-note placement rules after out-of-order Discord-fix batch inserts.
* fix(player): stable playbar clocks when showing remaining time
Pad seekbar time strings to a fixed width so ticking remaining time does not resize WaveformSeek via ResizeObserver; tighten clock-to-waveform spacing and keep the toggle icon inline.
* docs: CHANGELOG and credits for playbar remaining-time fix (PR #987)
* docs: CHANGELOG entry for playbar remaining-time waveform fix (#987)
* chore: drop settingsCredits entry for minor playbar fix (#987)
* docs: move #987 CHANGELOG entry to end of [1.47.0] Fixed section
* fix(ui): stop New badge flicker on mainstage album rail first hover
Dim cover via ::before under badges; keep play overlay transparent and
avoid visibility toggles on rail action buttons.
* docs: CHANGELOG for mainstage New badge hover fix (PR #986)
* fix(ui): keep New badge above rail cover zoom (match grid stacking)
Use contain:paint + translateZ layering like New Releases grid so scaled
cover art does not paint over the badge during hover animation.
* fix(ui): global album cover badge stacking for all rails and grids
Move img/badge/overlay z-index into album-card.css; stop album-grid from
resetting card transform or duplicating cover rules that hid New badges.
* docs: CHANGELOG — badge stacking applies to all album rails
* fix(ui): restore rail play buttons above scaled cover layer
Raise play overlay and badge z-index on horizontal rails so WebKit does not paint zoomed cover art over hover controls after the New badge stacking fix.
* fix(search): load album cover for song results
Live Search song rows carried the per-track `mf-…` coverArt id from
search3, which the cover pipeline does not resolve, so the thumbnails went
blank. Album rows worked because they use the album-scoped `al-…` id. A
song's search thumbnail is its album cover anyway — derive it from the
albumId so it loads, and show a music glyph when there is no album.
* docs: changelog for search song cover fix (#984)
* fix(search): reject FTS syntax chars in local index live search queries
FTS5 treats `=` and similar characters as query syntax, so tokens like
1=2 produced unrelated prefix hits. Skip FTS for unsafe tokens instead.
* docs: CHANGELOG for local index FTS syntax query fix (PR #983)
* fix(search): reject syntax junk in server search3 and live search race
Share FTS-safe token guard with search3; skip network invoke for ** and
similar queries; show empty dropdown without misleading source badge.
* fix(search): allow censorship stars in queries, reject wildcard-only tokens
Block ** and **** but keep ***Flawless-style title searches working
in local FTS and search3.
* fix(ui): square song-rail nav buttons to match other rails
The song rail's prev/next (and reroll) buttons were circular while every
other rail uses the square rounded-rect nav button. Align them.
* docs: changelog for square song-rail nav buttons (#982)
* fix(layout): collapse browse toolbar to icons on compact width
On a narrow window the labelled filter buttons wrapped into several rows
and, being a fixed-height sticky header, starved the album grid below them
down to a clipped strip. Wrap each toolbar label in a hideable span and drop
to icon-only in compact (mobile) mode; icons keep their tooltip and
aria-label so the action stays discoverable.
* fix(window): raise minimum window size to 520x640
The old 360x480 floor let the window shrink far past the point the layout
holds together. Floor it at a laptop-friendly size where the compact layout
(icon toolbar + scrollable lists) still works.
* fix(player): scale mobile cover to fit short windows
The cover width was viewport-derived with no height bound, so on a short
window the square grew past its slot and overlapped the title. Bind it to
the shrinkable wrap via max-height and keep it square with auto sizing.
* docs: changelog for small-window layout fixes (#981)
* fix(playlist): suggestion rows match the normal track row
The Suggested Songs table left the Favorite and Rating columns empty and
rendered only a single artist, so multi-artist tracks lost their split and
the blank columns read as a gap between Genre and Duration.
- Extract a shared PlaylistArtistCell that splits the OpenSubsonic artists
array into individually navigable links; use it for both the playlist
rows and the suggestions so a track reads the same before and after it is
added.
- Show the real favorite heart and star rating in suggestion rows (global
song operations), seeded from the song's own starred/userRating, removing
the empty-column gap.
* docs: changelog for suggestion row parity (#980)
* fix: show album artist on featured compilation cards
The 'Also featured on' cards are synthesised from search3 child songs,
which only read the flat `albumArtist` field. Compilation children leave
that empty — the album-artist credit lives in OpenSubsonic's structured
`albumArtists` / `displayAlbumArtist` — so the card fell back to the '—'
placeholder. Carry the structured credit (and the display string) onto
the synthesised album so it resolves a name (and stays navigable when the
server supplies an id).
* docs: changelog for featured-compilation artist credit (#979)
* fix: RC3 queue link underline, genre album artist split, Artists "Other" bucket
Three zunoz reports, one change:
- Queue now-playing card: the artist and album links now underline on hover
(not just recolour), matching clickable names everywhere else. The album
link sits on .queue-current-sub itself; artist links are nested .is-link
spans — both selectors covered.
- Genre album cards split multi-artist credits into individual links like the
rest of the app. Root cause was the local-index genre query hardcoding NULL
for the album raw_json column, so OpenSubsonic artists[] never reached the
card and it fell back to the flat "A • B" single link. Select a.raw_json
instead (parity with the All Albums / advanced-search album queries).
- Artists page alphabet index: # is now digits-only, and a new "Other" bucket
collects accented Latin (Æ/Ø/Å…) and non-Latin scripts (CJK, Cyrillic, …)
that previously fell into the # catch-all. Adds artistBucketKey/compareBuckets
helpers (unit-tested), an OTHER bucket sorted last, and the artists.other
label across 9 locales.
* docs(changelog): queue link hover, genre card artist split, Artists Other bucket (#977)
* fix(tracks): RC3 layout regressions + multi-artist links
- Restore vertical rhythm on the Tracks hub: the header/hero/rails/browse
sections sat two wrapper divs below .tracks-page so its flex gap never
reached them and they collapsed together. New .tracks-hub-stack re-applies
the gap, fixing the tagline-touching-hero and Random-Pick-overlapping-hero
spacing (the rail's nav buttons no longer ride into the box above).
- Stop the sticky browse-table header going transparent on hover: its base
background became opaque but the :hover rule still forced transparent, so
rows bled through. Header now keeps its card background on hover.
- Widen the Duration column (56px -> 72px; 56 -> 64 on mobile) so the
"DURATION" header no longer clips/overruns.
- Split multi-artist tracks into individually clickable artist links in both
the "Track of the moment" hero and the browse list rows (OpenSubsonic
artists[] with single-artist fallback), matching the album tracklist.
* docs(changelog): Tracks RC3 spacing, Duration, header hover, multi-artist (#976)
* fix(mainstage): rename Home Page setting, add start-page fallback + empty state
- Rename "Home Page" personalisation section to "Mainstage" across 9 locales
so the heading matches the sidebar entry; add "mainstage" search keyword.
- Index route "/" now redirects to the first visible library item when the
Mainstage sidebar entry is hidden, instead of stranding the app on a blank
page. New pure resolveStartRoute() mirrors sidebar order + nav-mode gating.
- Show a guided empty state on Mainstage when every section is toggled off,
with a CTA into Settings -> Personalisation.
- Unit tests for resolveStartRoute.
* docs(changelog): Mainstage rename, start-page fallback + empty state (#975)
* fix(ui): pointer cursor on the queue collapse handle
The round handle is a click-to-collapse button but showed the col-resize
cursor like the seam strip. Use a pointer on the handle; the seam keeps
col-resize and a real drag still switches the body cursor to col-resize.
* fix(favorites): make Plays, Last Played and BPM columns sortable
The header marked these columns sortable (pointer cursor) but handleSortClick
gated on a separate, narrower column set, so clicks did nothing. Both the
comparator and the header now share one SORTABLE_COLUMNS source, so the
affordance and the behaviour can't drift apart again.
* docs(changelog): queue handle cursor + favorites column sorting (#974)
* fix(random-mix): limit exclusion toggle click area to checkbox and title
The audiobook exclusion was a full-width label wrapping the checkbox,
title and description, so clicking empty space or the description text
toggled it. Make only the checkbox and its title clickable; the
description and surrounding space are no longer hit targets.
* fix(random-mix): stop excluding Thriller and Fantasy as audiobook genres
These keywords match regular music (e.g. Trance/Metal genre tags, a track
titled "Thriller") because the exclusion checks genre, title, album and
artist by substring, dropping a few legit songs per mix. Remove both from
the audiobook keyword list.
* docs(changelog): random mix audiobook exclusion fixes (#973)
* feat(tooltip): 2s open delay and shared tooltipAttrs helper
Add a 2s hover open delay in TooltipPortal (single behaviour source) so
tooltips no longer flash on quick pointer passes; hiding stays immediate.
Add tooltipAttrs() to pair data-tooltip with a matching aria-label for
buttons touched in the unification work. Covered by Vitest.
* feat(tooltip): lower open delay to 1s
2s felt too long in testing; 1s gives the same anti-flash behaviour
without making intentional hovers wait.
* feat(tooltip): action tooltips on the artist overview
Add tooltips describing the action to Last.fm, Wikipedia, Play All,
Shuffle and Radio. Shuffle/Radio now show a tooltip on desktop too,
not just mobile. Strings added to all 9 locales.
* feat(tooltip): action tooltips on the album overview
Add tooltips describing the action to the desktop Play, Artist Bio and
Download (ZIP) buttons, matching the mobile layout. Strings added to all
9 locales.
* feat(tooltip): action tooltips on the All Albums toolbar
Add tooltips describing the action to the sort, year and genre filter
buttons. SortDropdown gains an optional tooltip prop; the year and genre
filter components carry their own, so the tooltips also appear on the
other browse pages that reuse them. Strings added to all 9 locales.
* feat(tooltip): action tooltips on song-list rows
Add Play and Add-to-queue tooltips to the per-row icons in SongRow, used
by the Tracks browse list, Search and Advanced Search. Also localizes the
aria-labels, which were hardcoded English. New common.addToQueue in all 9
locales.
* fix(tooltip): uniform tooltip placement on the Artists toolbar
The favourite and multi-select buttons forced tooltips below while the
view-mode buttons auto-flipped above, so the row looked inconsistent.
Pin the view-mode buttons below too, matching the rest of the row and the
Albums toolbar.
* feat(tooltip): clarify and align the Advanced Search scope row
Add a leading "Search in:" label and per-chip tooltips so the
All/Artists/Albums/Songs row reads as a scope limiter. Drop the forced
below-placement on the small star filter (used only here) so the
favourites chip flips with the others instead of sitting alone below.
Strings added to all 9 locales.
* docs(changelog): tooltip unification (#972)
* fix(playlists): Smart Playlist editor theme, toggles, and exclude-all-genres
Replace native sort select with CustomSelect, fix mode-button layout shift,
color-code included vs excluded genres, collapse exclude-all to untagged rule,
and handle empty smart playlists without false "not found".
* docs: CHANGELOG and credits for Smart Playlist editor fix (PR #970)
* chore(credits): drop minor fix entries from PR #958 onward
* fix(ui): shrink-wrap floating player bar instead of full-width strip
The bar used fixed left and right insets, which stretched its background
across the whole main column. Center it with max-content width so only
the pill-shaped controls are painted, and soften the drop shadow.
* docs: note PR #969 in changelog and settings credits
* fix(settings): improve in-page search matching and coverage
Index AudioMuse and individual shortcut rows, tighten fuzzy matching so
junk queries return no hits, and scroll to the parent subsection when a
shortcut result is selected.
* docs: note PR #968 in changelog and settings credits
* fix(player): clamp custom delay input and align preview with armed timer
Reject absurd custom minute values that overflow setTimeout, share one
delay helper between the modal preview and schedule actions, and refresh
countdown ticks when a new deadline is armed.
* docs: note PR #967 in changelog and settings credits
* fix(artist-detail): keep ext-link border visible on hover
Hover used --border-subtle, which on Catppuccin matches --bg-card and
visually erased the rim while only the fill changed. Match btn-surface:
--ctp-surface1 border, --ctp-overlay0 on hover.
* docs(changelog): credit zunoz on Psysonic Discord for PR #966
* fix(playlists): tooltips on Play/Add Songs and song count pluralization
Add data-tooltip to Play and Add Songs in playlist hero; switch
playlists.songs to count-based _one/_other forms (was {{n}} without
i18next plural suffix, breaking spacing and singular).
* fix(playlists): render BPM and optional cols in Suggested Songs rows
PlaylistSuggestions shared column headers with the main tracklist but
its row switch omitted bpm, genre, playCount, and lastPlayed.
* fix(random-mix): honor keyword blocks and scope genre list to library
Keyword filter was gated behind the audiobook exclusion checkbox, so
blocked artists still appeared after Remix. Genre Mix now loads genres
via fetchGenreCatalog (scoped index / library filter) instead of raw
getGenres; show empty state when all tracks are filtered out.
* docs(changelog): credit zunoz on Psysonic Discord for PR #965
* fix(home): scope Because you listened rail to sidebar library
Similar-artist album picks used getArtist without library filtering;
session cache also ignored musicLibraryFilterVersion. Filter picks to
the scoped album set and key cache/reserve by filter version.
* docs(changelog): credit zunoz on Psysonic Discord for PR #964
* test: satisfy SubsonicAlbum required fields in new unit tests
Fix tsc --noEmit CI: songCount and duration are required on SubsonicAlbum.
* fix(composers): drop Navidrome role rows with zero composer albums
Navidrome can list performer-only artists under role=composer with
stats.composer.albumCount 0; filter them out of the Composers catalog
so search no longer surfaces ghost entries like Apollo 440.
* docs(changelog): credit zunoz on Psysonic Discord for PR #963
* fix(artists): inset selection ring on grid cards, stop composer hover clip
Artist multi-select used a positive outline-offset that clipped in the
first grid row and sat outside the card border on hover. Match album
cards with an inset ring; drop composer-card hover translateY that
sheared the top edge in the in-page scrollport.
Reported by zunoz (v1.47.0-rc.3).
* fix(cards): selection ring via inset ::after overlay on WebKitGTK grids
Replace outline-based multi-select rings on album/artist/playlist cards
with the same inset ::after box-shadow pattern used for card focus rings
(card.css) — avoids clipping and the 1px gap vs the inner border on
overflow:hidden tiles in All Albums and related browse grids.
* docs: note browse grid selection ring fix in CHANGELOG (PR #962)
* docs(changelog): credit zunoz on Psysonic Discord for PR #962
* fix(composers): keep role-split names in page-local search
Composers browse already loads the Navidrome role-scoped catalog; scoped
search now filters that list instead of replacing it with generic artist
index/search3 hits that merge split credits into one joined name.
Reported by zunoz on the Psysonic Discord (v1.47.0-rc.3).
* docs: note Composers scoped-search fix in CHANGELOG (PR #961)
* fix(now-playing): split artist links and About the Artist tabs
OpenSubsonic artists[] now drives per-artist navigation on Now Playing
hero and the queue current-track row (matching player bar). About the
Artist loads bio for each performer via tabs when a track has multiple
artist ids; queue Info uses the primary ref for bio/tour fetch.
Reported by zunoz on the Psysonic Discord (v1.47.0-rc.3).
* docs: note Now Playing multi-artist fix in CHANGELOG (PR #960)
* fix(albums): scope All Albums genre filter to selected music library
When the sidebar narrows to one Subsonic library, the genre popover fell back
to server-wide getGenres() instead of the scoped local index catalog. Load
genre options from libraryGetGenreAlbumCounts for library-only scope and
enable the catalog path whenever the index is on and a library is selected.
* docs: credit All Albums genre filter fix (PR #959, report zunoz)
* chore: drop settingsCredits entry for small genre-filter fix
* fix(player): persist volume/repeat in dedicated localStorage key
Player prefs were bundled with the full queue blob in psysonic-player; after
thin-state #872 a large queue can exceed the quota and safeStorage silently
drops every write, so volume and repeat mode stopped surviving restarts.
Move them to psysonic_player_prefs, gate main-blob writes until rehydrate,
and sync volume to the Rust engine on startup.
* fix(player): split queue visibility and Last.fm cache from main persist blob
Move isQueueVisible and lastfmLovedCache to dedicated localStorage keys so
they keep saving when psysonic-player hits the quota on large queues. Include
the new keys in settings backup export.
* docs: note player prefs persist fix in CHANGELOG and credits (PR #958)
* fix(focus): keyboard focus ring no longer clipped by overflow or cover
The global :focus-visible ring used a positive outline-offset, so it was
drawn outside the element and clipped by any ancestor with overflow:hidden
or a scroll container (cards, rails, player bar, queue strip). Draw it inset
instead via shared --focus-ring-* variables (single source). Cards draw the
ring as an overlay above the cover, since a cover's own stacking context
(transform/contain for render stability) would otherwise paint over an inset
outline. Dracula now only sets --focus-ring-color.
* refactor(focus): fold scattered focus-ring overrides onto shared knob
Six components declared their own :focus-visible outline (mostly an exact copy
of the old global ring). Remove the redundant ones so they inherit the global
inset ring; keep the custom-coloured ones (genre pill, playback-delay modal)
but source width/offset from --focus-ring-*; move the because-card ring to the
central card focus ring (it has a cover and needs the lifted treatment).
* fix(settings): round theme accordion inner box to match its section
The theme picker's inner accordion had square corners, so the first (open)
group header ran flush into the rounded Theme card. Give .theme-accordion a
border-radius + overflow:hidden so its top/bottom corners continue the parent
card's rounding, matching the other settings sections.
* fix(search): unify search fields to one look
Live-search, Help and Settings search differed (pill vs rounded-rect, glow vs
plain border-change, mismatched backgrounds). Align them on the canonical input
look: radius-md, ctp-base background, accent border + soft accent-dim focus
glow. Drop the live-search pill radius, and suppress the input's own inset ring
so only the outer cluster glow shows (no double ring).
* fix(tracklist): column picker menu no longer clipped on short lists
The column-visibility dropdown was an absolutely-positioned menu inside the
tracklist, so a short list (e.g. a one-song Favorites view) clipped it via the
ancestor's overflow box. Render the menu in a portal to <body> with fixed
positioning anchored to the trigger (flips above when there's no room below),
following on scroll/resize. Outside-click + Escape close now live in the shared
TracklistColumnPicker (the menu is portalled out of the wrapper, so the old
wrapper-only check would have closed it on every in-menu click). Fixes albums,
playlists and favorites in one shared place. Adds a behaviour test.
* docs(changelog): UI/CSS fixes pass (#954)
* feat(tracks): distinct circular song cards with jump-to-album badge
Single-track cards looked identical to album tiles, so clicking the body
read as album behaviour even though it starts playback. Give them a round
vinyl-style cover (square stays album-only) via a shared .cover-circle
utility, and add a 'To album' badge under the artist that navigates to the
track's album. Card click still plays; the badge is the explicit nav path.
* i18n(tracks): add toAlbum label across 9 locales
* docs(changelog): track-card redesign + to-album badge (#953)
* fix(cover): follow connect-URL flips in library cover backfill
The native cover backfill was configured once with a snapshot of the runtime
connect URL. When a laptop moved off the LAN, the smart endpoint switch flipped
the sticky connect URL to the public address (playback/UI covers rebuild it per
request and followed it), but backfill kept fetching from the now-unreachable
local address — flooding the log with "error sending request" failures.
Make the connect cache observable (notify on effective flips) and have the
backfill hook reconfigure when the resolved URL changes, forcing a pass so the
.fetch-failed backoff from the stale address is cleared and those covers retry
on the reachable endpoint.
* docs(changelog): note cover backfill endpoint-switch fix (PR #952)
* fix(cover): abort stale backfill pass + rerun on connect-URL flip
Frontend reconfigure alone didn't fully cover the boot case: at startup the
first backfill pass starts on the primary (LAN) URL before the reachability
probe resolves, so when the probe flips to public the forced rerun was dropped
by the pass_running guard and the slow LAN pass (every cover timing out) ran to
completion with nothing re-running on the reachable address.
set_session now bumps a session generation; the running pass checks it on every
focus gate and abandons promptly when the URL flips (same server_index_key, new
rest_base_url). try_schedule_full_pass records a rerun when a pass is in flight
and drains it once the abandoned pass returns, so a fresh forced pass runs on
the new address.
* fix(cover-backfill): resolve connect URL per fetch instead of baking it into the queue
The backfill worklist no longer carries a URL. Each cover fetch reads the
current reachable address live from a single worker cell, so a LAN↔public flip
is honoured even by the pass already in flight — its remaining covers download
against the new endpoint without aborting/rebuilding the worklist.
- Drop rest_base_url from CoverBackfillSession; add live base_url cell read in
ensure_one. Remove the session-generation abort machinery (no longer needed).
- New lightweight library_cover_backfill_set_base_url command pushes the URL on
every connect-cache flip; a real change clears the stale .fetch-failed backoff
and runs a forced pass so covers that timed out on the old address retry.
- Split useLibraryCoverBackfill into a configure effect (server/creds/strategy)
and a flip effect that only pushes the URL.
- Keep a single rerun_pending flag for the boot case (flip mid-pass), since the
finished pass re-arms the idle gate.
* perf(analysis): measure tpm over trailing 5s instead of full minute
Mirror the cover cpm change: a 60s rolling average added too much inertia and
flattened real bursts/stalls. Count completions in the trailing 5s window and
extrapolate to per-minute, so analysis tpm reacts promptly and decays to 0
within the window when idle. Retention stays at 60s.
* docs(changelog): note trailing-window throughput rate (PR #948)
* feat(perf): add on-demand (ui) throughput to cover pipeline cpm
Cover cpm previously measured only the native backfill (lib) via the
cover:library-progress done delta. Add a parallel UI series: every completed
on-demand Rust cover ensure (grid / now-playing) records a timestamp, surfaced
as a covers-per-minute rate. Both are exposed in the live cover diag, shown as
separate Backfill (lib) / On-demand (ui) cards in the Monitor tab (each pinnable
to the overlay) and as lib/ui rows in the Cover pipeline overlay block.
* docs(changelog): note cover on-demand (ui) throughput (PR #947)
Add CHANGELOG entry and credits line for the UI cover cpm metric.
* fix(perf): source on-demand (ui) cpm from backend produced-cover count
The JS ensure-queue counter never tracked: produced covers return hit:true
(only misses/errors are hit:false), and ensure-queue dedup/HMR made client
counting unreliable. Count on-demand covers natively in ensure_inner on the
produce path (non-bulk, past the cache-hit gate), expose a cumulative
uiEnsuredTotal in the pipeline stats, and derive the per-minute rate on the
frontend from polled deltas — mirroring the lib backfill series.
* perf(cover): measure cpm over trailing 5s instead of full minute
A 60s rolling average added too much inertia, flattening real bursts and
stalls in both the lib backfill and on-demand (ui) cover throughput. Compute
the rate from the trailing 5s of samples (still extrapolated to per-minute),
so the figure reacts promptly and decays to 0 within the window when idle.