diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 968af53..e650b05 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -108,7 +108,10 @@ Plans:
3. Search results respect the active library filter — searching with a library selected returns only matches from that library; with "All Libraries" selected, searches everything
4. Playlists can contain tracks from multiple libraries — adding tracks from different libraries to the same playlist works naturally
5. When a library is removed, its tracks in playlists become phantom entries — visually distinguished (greyed out / icon) with preserved title, artist, album metadata instead of disappearing
-**Plans:** TBD
+**Plans:** 2 plans
+Plans:
+- [ ] 13-01-PLAN.md — Backend library-filtered sqlc queries + Go methods + FTS search
+- [ ] 13-02-PLAN.md — Frontend library filter store + dropdown UI + all view/search wiring + verification
### Phase 14: Performance Optimization
**Goal:** Scrolling, navigation, and rendering are as smooth and fast as possible — scrolling feels like a native animation, navigation is instant, no unnecessary re-renders
@@ -143,7 +146,7 @@ Plans:
| 10. Schema & Migration | 2/2 | Complete | 2026-03-09 | - |
| 11. Per-Library Scan Pipeline | 3/3 | Complete | 2026-03-09 | - |
| 12. Library CRUD & Data Integrity | v1.1 | Complete | 2026-03-15 | 2026-03-15 |
-| 13. Library Views & Phantom Tracks | v1.1 | 0/? | Not started | - |
+| 13. Library Views & Phantom Tracks | v1.1 | 0/2 | Planned | - |
| 14. Performance Optimization | 4/4 | Complete | 2026-03-15 | - |
---
diff --git a/.planning/phases/13-library-views-phantom-tracks/13-01-PLAN.md b/.planning/phases/13-library-views-phantom-tracks/13-01-PLAN.md
new file mode 100644
index 0000000..c0dd242
--- /dev/null
+++ b/.planning/phases/13-library-views-phantom-tracks/13-01-PLAN.md
@@ -0,0 +1,247 @@
+---
+phase: 13-library-views-phantom-tracks
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/database/sql/queries/audio_files.sql
+ - backend/database/sql/queries/release_groups.sql
+ - backend/database/sql/queries/artists.sql
+ - backend/database/sql/queries/genres.sql
+ - backend/database/search.go
+ - backend/library/query.go
+autonomous: true
+requirements: [VIEW-01, VIEW-02, VIEW-03, VIEW-04]
+
+must_haves:
+ truths:
+ - "Backend returns all tracks when no library filter is active (unified view)"
+ - "Backend returns only tracks from a specific library when library_id is provided"
+ - "Albums, artists, and genres are filtered to only show entities that have tracks in the selected library"
+ - "FTS5 search returns results scoped to a specific library when library_id is provided"
+ - "All existing unfiltered queries continue to work unchanged"
+ artifacts:
+ - path: "backend/database/sql/queries/audio_files.sql"
+ provides: "GetAllTracksWithFullMetadataByLibrary query"
+ contains: "WHERE af.library_id"
+ - path: "backend/database/sql/queries/release_groups.sql"
+ provides: "GetAllAlbumsWithDetailsByLibrary, GetAlbumsByArtistByLibrary queries"
+ contains: "WHERE af.library_id"
+ - path: "backend/database/sql/queries/artists.sql"
+ provides: "GetAlbumArtistsByLibrary query"
+ contains: "WHERE af.library_id"
+ - path: "backend/database/sql/queries/genres.sql"
+ provides: "GetAllGenresWithCountsByLibrary, GetTracksByGenreByLibrary queries"
+ contains: "WHERE af.library_id"
+ - path: "backend/database/search.go"
+ provides: "SearchFTSTracksByLibrary method"
+ contains: "AND tm.library_id"
+ - path: "backend/library/query.go"
+ provides: "GetAllTracksByLibrary, GetAllAlbumsByLibrary, GetAllArtistsByLibrary, GetAllGenresWithCountsByLibrary, GetTracksByGenreByLibrary, GetAlbumsByArtistByLibrary, GetAlbumTracksByLibrary, SearchTracksByLibrary methods"
+ exports: ["GetAllTracksByLibrary", "SearchTracksByLibrary"]
+ key_links:
+ - from: "backend/library/query.go"
+ to: "backend/database/sql/queries/*.sql"
+ via: "sqlc-generated Queries methods"
+ pattern: "l\\.db\\.Queries\\."
+ - from: "backend/library/query.go"
+ to: "backend/database/search.go"
+ via: "l.db.SearchFTSTracksByLibrary"
+ pattern: "SearchFTSTracksByLibrary"
+---
+
+
+Add library-filtered SQL query variants for all browse views and FTS search so the frontend can request data scoped to a specific library.
+
+Purpose: Phase 13 requires backend filtering for 150K+ track collections (per CONTEXT.md locked decision). Every existing unfiltered query that powers a browse view needs a `ByLibrary` variant accepting a `library_id` parameter. Existing unfiltered queries remain unchanged for the "All Libraries" default view.
+
+Output: sqlc-generated query methods + Go wrapper methods on Library struct + filtered FTS search method on DB struct
+
+
+
+@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
+@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/13-library-views-phantom-tracks/13-CONTEXT.md
+
+
+
+
+From backend/library/query.go — existing types to reuse:
+```go
+type Track struct {
+ TrackName, ArtistName, TrackLength, FilePath string
+ TrackNumber, DiscNumber int64
+ Album string; Genre []string; Year int64
+ Composer, FileType string
+ SampleRate, BitDepth, Channels, Bitrate, FileSize int64
+}
+
+type Artist struct { ID int64; Name string }
+
+type Album struct {
+ ID int64; Name, ArtistName, CoverArtPath string
+ CoverArtSmall, CoverArtMedium, CoverArtLarge string
+ Year int64
+}
+
+type GenreWithCount struct { Name string; TrackCount int64 }
+
+// Helper used by GetAllTracks, SearchTracks, GetTracksByGenre:
+func mapTrackRow(...) Track
+```
+
+From backend/database/search.go — existing FTS search:
+```go
+type SearchTrackRow struct {
+ FilePath string; LengthMilliseconds int64
+ Title, ArtistName string
+ TrackNumber, DiscNumber sql.NullInt64
+ Album, Genre string; Year int64
+ Composer, FileType string
+ SampleRate, BitDepth, Channels, Bitrate, FileSize int64
+}
+
+func (d *DB) SearchFTSTracks(query string, limit int) ([]SearchTrackRow, error)
+```
+
+From track_metadata VIEW (already includes library_id):
+```sql
+CREATE VIEW IF NOT EXISTS track_metadata AS
+SELECT af.id, af.file_path, ... af.library_id
+FROM audio_files af
+LEFT JOIN recordings r ON ...
+LEFT JOIN release_group_recordings rgr ON ...
+LEFT JOIN release_groups rg ON ...
+LEFT JOIN artist_credits ac ON ...
+LEFT JOIN artists a ON ...
+LEFT JOIN cover_art ca ON ...
+```
+
+
+
+
+
+
+ Task 1: Add library-filtered sqlc queries for all browse views
+
+ backend/database/sql/queries/audio_files.sql
+ backend/database/sql/queries/release_groups.sql
+ backend/database/sql/queries/artists.sql
+ backend/database/sql/queries/genres.sql
+
+
+Add `ByLibrary` variants of each query used by browse views. Each variant is a copy of the existing query with an added `WHERE af.library_id = ?` condition (or equivalent JOIN condition). The `track_metadata` VIEW already includes `af.library_id` as the last column.
+
+**audio_files.sql** — add these queries:
+
+1. `GetAllTracksWithFullMetadataByLibrary` — copy of `GetAllTracksWithFullMetadata` (line 75) but add `WHERE af.library_id = ?1` to the outer query. The existing query JOINs audio_files, so the filter goes on `af.library_id`. Use sqlc parameter annotation `-- :arg library_id`.
+
+2. `GetAudioFilesByReleaseGroupByLibrary` — copy of `GetAudioFilesByReleaseGroup` (line 139) but add `AND af.library_id = ?` alongside the existing `WHERE rgr.release_group_id = ?`.
+
+**release_groups.sql** — add these queries:
+
+3. `GetAllAlbumsWithDetailsByLibrary` — copy of `GetAllAlbumsWithDetails` (line 48). This query doesn't directly JOIN audio_files, so add an `EXISTS (SELECT 1 FROM audio_files af WHERE af.library_id = ? AND EXISTS (SELECT 1 FROM recordings r JOIN release_group_recordings rgr ON rgr.recording_id = r.id WHERE rgr.release_group_id = rg.id AND r.id = (SELECT recording_id FROM audio_files WHERE id = af.id)))` — actually simpler: add `WHERE rg.id IN (SELECT DISTINCT rgr2.release_group_id FROM release_group_recordings rgr2 JOIN recordings r2 ON r2.id = rgr2.recording_id JOIN audio_files af2 ON af2.recording_id = r2.id WHERE af2.library_id = ?)`. Check the existing query structure first and find the simplest approach. Likely: wrap the existing query body and add a subquery filter on `rg.id` to only include albums that have at least one track in the given library.
+
+4. `GetAlbumsByArtistByLibrary` — copy of `GetAlbumsByArtist` (line 67). Add the same album-in-library subquery filter alongside the existing artist_id filter.
+
+**artists.sql** — add:
+
+5. `GetAlbumArtistsByLibrary` — copy of `GetAlbumArtists` (line 34). Filter to only artists that have at least one album with at least one track in the given library. Use subquery: `WHERE a.id IN (SELECT DISTINCT ac2.artist_id FROM artist_credits ac2 JOIN release_groups rg2 ON rg2.artist_credit_id = ac2.id JOIN release_group_recordings rgr2 ON rgr2.release_group_id = rg2.id JOIN recordings r2 ON r2.id = rgr2.recording_id JOIN audio_files af2 ON af2.recording_id = r2.id WHERE af2.library_id = ?)`.
+
+**genres.sql** — add:
+
+6. `GetAllGenresWithCountsByLibrary` — copy of `GetAllGenresWithCounts` (line 66). Filter track counts to only count tracks in the given library. The existing query JOINs through recording_genres → recordings → audio_files, so add `AND af.library_id = ?` to the existing JOINs.
+
+7. `GetTracksByGenreByLibrary` — copy of `GetTracksByGenre` (line 26). Add `AND af.library_id = ?` alongside the existing genre name filter.
+
+After adding all queries, run `make generate` (or `sqlc generate` from `backend/database/`) to regenerate Go code. Verify compilation with `go build -tags webkit2_41 ./...`.
+
+**Important:** Do NOT modify existing queries — only add new ones. The unfiltered variants serve the "All Libraries" default view.
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && go generate ./backend/database/... && go build -tags webkit2_41 ./...
+
+ Seven new sqlc queries exist (ByLibrary variants), sqlc generate succeeds, go build compiles cleanly
+
+
+
+ Task 2: Add library-filtered Go query methods and FTS search
+
+ backend/library/query.go
+ backend/database/search.go
+
+
+**backend/library/query.go** — Add `ByLibrary` wrapper methods that mirror each existing method but accept `libraryID int64` and call the `ByLibrary` sqlc query variant. Reuse `mapTrackRow` and cover art URL resolution logic identically.
+
+Add these exported methods to the Library struct:
+
+1. `GetAllTracksByLibrary(libraryID int64) ([]Track, error)` — calls `l.db.Queries.GetAllTracksWithFullMetadataByLibrary(l.ctx, libraryID)`, maps via `mapTrackRow`. Do NOT return `errNoTracksInLibrary` for empty result — a library with no tracks is valid (not an error). Return empty slice.
+
+2. `GetAllAlbumsByLibrary(libraryID int64) ([]Album, error)` — calls `GetAllAlbumsWithDetailsByLibrary`, maps with cover art URL resolution.
+
+3. `GetAllArtistsByLibrary(libraryID int64) ([]Artist, error)` — calls `GetAlbumArtistsByLibrary`.
+
+4. `GetAlbumsByArtistByLibrary(artistID, libraryID int64) ([]Album, error)` — calls `GetAlbumsByArtistByLibrary`.
+
+5. `GetAllGenresWithCountsByLibrary(libraryID int64) ([]GenreWithCount, error)` — calls `GetAllGenresWithCountsByLibrary`.
+
+6. `GetTracksByGenreByLibrary(genreName string, libraryID int64) ([]Track, error)` — calls `GetTracksByGenreByLibrary`.
+
+7. `GetAlbumTracksByLibrary(albumID, libraryID int64) ([]Track, error)` — calls `GetAudioFilesByReleaseGroupByLibrary`.
+
+8. `SearchTracksByLibrary(query string, libraryID int64) ([]Track, error)` — calls `l.db.SearchFTSTracksByLibrary(query, searchTrackLimit, libraryID)`, maps via `mapTrackRow`.
+
+**backend/database/search.go** — Add `SearchFTSTracksByLibrary`:
+
+```go
+func (d *DB) SearchFTSTracksByLibrary(
+ query string, limit int, libraryID int64,
+) ([]SearchTrackRow, error)
+```
+
+Copy from `SearchFTSTracks` but add `AND tm.library_id = ?` to the WHERE clause and pass `libraryID` as the third parameter. The hand-crafted SQL already JOINs `track_metadata tm ON tm.id = si.rowid`, so the filter is trivial. Add SAFETY comment following project convention.
+
+All methods must follow project conventions:
+- Error wrapping with `fmt.Errorf("...: %w", err)`
+- slog structured logging with method context
+- godot doc comments ending with period
+- Lines under 100 chars (break as needed)
+- nlreturn blank line after error returns
+
+Verify with `go build -tags webkit2_41 ./...` and `make lint`.
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && go build -tags webkit2_41 ./... && make lint
+
+ Eight new Go methods on Library struct + one new SearchFTSTracksByLibrary on DB struct; all compile cleanly, lint passes. Wails binding generation will pick up the new exported methods automatically.
+
+
+
+
+
+- `go build -tags webkit2_41 ./...` compiles without errors
+- `make lint` passes (golangci-lint v2 with strict rules)
+- `sqlc generate` succeeds in backend/database/
+- All seven new ByLibrary SQL queries exist in their respective .sql files
+- All eight new Go methods exist on Library struct
+- SearchFTSTracksByLibrary exists on DB struct
+- Existing unfiltered queries and methods are unchanged
+
+
+
+- Backend can serve track/album/artist/genre data filtered to a specific library_id
+- Backend can serve FTS5 search results filtered to a specific library_id
+- All new methods are Wails-bindable (exported, on exported struct)
+- No regression in existing unfiltered queries
+
+
+
diff --git a/.planning/phases/13-library-views-phantom-tracks/13-02-PLAN.md b/.planning/phases/13-library-views-phantom-tracks/13-02-PLAN.md
new file mode 100644
index 0000000..aacab06
--- /dev/null
+++ b/.planning/phases/13-library-views-phantom-tracks/13-02-PLAN.md
@@ -0,0 +1,360 @@
+---
+phase: 13-library-views-phantom-tracks
+plan: 02
+type: execute
+wave: 2
+depends_on: ["13-01"]
+files_modified:
+ - frontend/src/store/library-store.ts
+ - frontend/src/store/controllers/library-controller.ts
+ - frontend/src/components/library-filter/library-filter.ts
+ - frontend/index.html
+ - frontend/index.ts
+ - frontend/src/components/track-list/track-list.ts
+ - frontend/src/components/cover-grid/cover-grid.ts
+ - frontend/src/components/artists-view/artists-view.ts
+ - frontend/src/components/genres-view/genres-view.ts
+ - frontend/src/components/artist-details/artist-details.ts
+ - frontend/src/components/genre-details/genre-details.ts
+ - frontend/src/components/search-bar/search-bar.ts
+autonomous: false
+requirements: [VIEW-01, VIEW-02, VIEW-03, VIEW-04, PLAY-01, PLAY-02, PLAY-03]
+
+must_haves:
+ truths:
+ - "Default view shows tracks from all libraries merged (unified presentation)"
+ - "User can select a specific library from a dropdown in the top bar and all views show only that library's content"
+ - "Search results respect the active library filter"
+ - "Switching library filter triggers a backend re-fetch with loading state"
+ - "Scroll positions reset when switching library filter"
+ - "Playlists always show all tracks regardless of library filter"
+ - "Phantom tracks appear with existing phantom styling when a library is removed"
+ - "Detail views (artist, genre) respect the active library filter"
+ - "Library filter resets to All Libraries on app restart (no persistence)"
+ artifacts:
+ - path: "frontend/src/components/library-filter/library-filter.ts"
+ provides: "Library filter dropdown component"
+ min_lines: 60
+ - path: "frontend/src/store/library-store.ts"
+ provides: "selectedLibraryId state + filtered fetch logic"
+ contains: "selectedLibraryId"
+ - path: "frontend/src/store/controllers/library-controller.ts"
+ provides: "selectedLibraryId getter/setter pass-through"
+ contains: "selectedLibraryId"
+ - path: "frontend/index.html"
+ provides: "library-filter element in top bar"
+ contains: ""
+ key_links:
+ - from: "frontend/src/store/library-store.ts"
+ to: "@go/library/Library"
+ via: "GetAllTracksByLibrary / GetAllTracks conditional call"
+ pattern: "GetAllTracksByLibrary|GetAllTracks"
+ - from: "frontend/src/components/library-filter/library-filter.ts"
+ to: "frontend/src/store/library-store.ts"
+ via: "libraryStore.setSelectedLibrary()"
+ pattern: "setSelectedLibrary"
+ - from: "frontend/src/components/track-list/track-list.ts"
+ to: "frontend/src/store/library-store.ts"
+ via: "libraryCtrl.getTracks() (now library-aware)"
+ pattern: "getTracks"
+---
+
+
+Add library filter state to the frontend store, a compact dropdown control in the top bar, and wire all browse views + search to respect the active library filter.
+
+Purpose: Users need to filter their entire music collection to a single library or view all merged. This plan adds the filter UI and connects it to all views via the existing store/controller/component pattern. Cross-library playlists and phantom tracks already work via existing infrastructure (Phase 10 schema + Phase 12 CRUD pre-populate phantom metadata + playlist-details phantom rendering) — this plan verifies they work correctly in the multi-library context.
+
+Output: Working library filter dropdown, all views respond to filter changes, search respects filter, playlists remain unfiltered, phantom tracks verified
+
+
+
+@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
+@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/13-library-views-phantom-tracks/13-CONTEXT.md
+@.planning/phases/13-library-views-phantom-tracks/13-01-SUMMARY.md
+
+
+
+
+From backend/library/query.go — new methods added by Plan 13-01:
+```go
+func (l *Library) GetAllTracksByLibrary(libraryID int64) ([]Track, error)
+func (l *Library) GetAllAlbumsByLibrary(libraryID int64) ([]Album, error)
+func (l *Library) GetAllArtistsByLibrary(libraryID int64) ([]Artist, error)
+func (l *Library) GetAlbumsByArtistByLibrary(artistID, libraryID int64) ([]Album, error)
+func (l *Library) GetAllGenresWithCountsByLibrary(libraryID int64) ([]GenreWithCount, error)
+func (l *Library) GetTracksByGenreByLibrary(genreName string, libraryID int64) ([]Track, error)
+func (l *Library) GetAlbumTracksByLibrary(albumID, libraryID int64) ([]Track, error)
+func (l *Library) SearchTracksByLibrary(query string, libraryID int64) ([]Track, error)
+// Existing unfiltered methods remain unchanged
+```
+
+From frontend/src/store/library-store.ts — current state shape:
+```typescript
+class LibraryStore {
+ private tracks: library.Track[] | null;
+ private albums: library.Album[] | null;
+ private artists: library.Artist[] | null;
+ private genres: library.GenreWithCount[] | null;
+ // Loading flags, scroll positions, changeGen, coverSize...
+ async getTracks(): Promise // calls GetAllTracks()
+ async getAlbums(): Promise // calls GetAllAlbums()
+ async getArtists(): Promise // calls GetAllArtists()
+ async getGenres(): Promise // calls GetAllGenresWithCounts()
+ async getAlbumsByArtist(id: number): Promise
+ private invalidate(): void // nulls caches + changeGen++ + eagerFetch()
+}
+```
+
+From frontend/src/store/library-store.ts — existing imports:
+```typescript
+import { GetAllTracks, GetAllAlbums, GetAllArtists, GetAllGenresWithCounts, GetAlbumsByArtist } from '@go/library/Library';
+```
+
+After Plan 13-01 + Wails binding regen, these will also be available:
+```typescript
+import { GetAllTracksByLibrary, GetAllAlbumsByLibrary, GetAllArtistsByLibrary,
+ GetAllGenresWithCountsByLibrary, GetAlbumsByArtistByLibrary,
+ GetTracksByGenreByLibrary, GetAlbumTracksByLibrary,
+ SearchTracksByLibrary } from '@go/library/Library';
+```
+
+From backend/library/query.go — library info for dropdown:
+```go
+func (l *Library) GetAllLibrariesWithTrackCounts() ([]Info, error)
+// Info struct: { ID int64, Name string, Path string, TrackCount int64 }
+```
+
+Already available as Wails binding:
+```typescript
+import { GetAllLibrariesWithTrackCounts } from '@go/library/Library';
+```
+
+From frontend/index.html — top bar structure:
+```html
+
+
+
YellowJacket
+
Music how it was meant to bee.
+
+
+
+```
+
+From frontend/src/components/genre-details/genre-details.ts — direct Wails binding:
+```typescript
+import { GetTracksByGenre } from '@go/library/Library';
+// calls GetTracksByGenre(this.genreName) directly, bypasses library store
+```
+
+From frontend/src/components/track-list/track-list.ts — search integration:
+```typescript
+import { SearchTracks } from '@go/library/Library';
+// loadTracks() calls libraryCtrl.getTracks() for browse
+// handleSearchResult() calls SearchTracks(term) for search
+```
+
+
+
+
+
+
+ Task 1: Add library filter state to store + controller, create dropdown component, wire all views
+
+ frontend/src/store/library-store.ts
+ frontend/src/store/controllers/library-controller.ts
+ frontend/src/components/library-filter/library-filter.ts
+ frontend/index.html
+ frontend/index.ts
+ frontend/src/components/track-list/track-list.ts
+ frontend/src/components/cover-grid/cover-grid.ts
+ frontend/src/components/artists-view/artists-view.ts
+ frontend/src/components/genres-view/genres-view.ts
+ frontend/src/components/artist-details/artist-details.ts
+ frontend/src/components/genre-details/genre-details.ts
+ frontend/src/components/search-bar/search-bar.ts
+
+
+**Step 1: Library store filter state** (`library-store.ts`)
+
+Add a `selectedLibraryId: number | null` field to LibraryStore (null = "All Libraries"). Add methods:
+- `getSelectedLibraryId(): number | null` — returns current filter
+- `setSelectedLibrary(id: number | null): void` — sets filter, calls `invalidate()` which clears caches, resets scroll positions, and triggers `eagerFetch()`. The existing invalidation + eager refetch pattern handles everything.
+- `getLibraries(): Promise` — calls `GetAllLibrariesWithTrackCounts()`. Cache the result in a `private libraries: library.Info[] | null` field. Invalidate on `LibraryAdded`, `LibraryRenamed`, `LibraryRemoved` events (the last two listeners already exist — extend them).
+
+Modify `getTracks()`: if `selectedLibraryId` is not null, call `GetAllTracksByLibrary(this.selectedLibraryId)` instead of `GetAllTracks()`. Similarly for `getAlbums()` → `GetAllAlbumsByLibrary`, `getArtists()` → `GetAllArtistsByLibrary`, `getGenres()` → `GetAllGenresWithCountsByLibrary`.
+
+Modify `getAlbumsByArtist(artistID)`: if `selectedLibraryId` is not null, call `GetAlbumsByArtistByLibrary(artistID, this.selectedLibraryId)` instead of `GetAlbumsByArtist(artistID)`.
+
+Add imports for the new Wails bindings: `GetAllTracksByLibrary`, `GetAllAlbumsByLibrary`, `GetAllArtistsByLibrary`, `GetAllGenresWithCountsByLibrary`, `GetAlbumsByArtistByLibrary`, `GetAllLibrariesWithTrackCounts`.
+
+Also add `getAlbumsByArtistNameCached()`: when `selectedLibraryId` is set, this should return null (force a backend query instead of client-side filtering, since cached albums are already library-filtered).
+
+**Step 2: Library controller pass-through** (`library-controller.ts`)
+
+Add pass-through methods:
+- `get selectedLibraryId(): number | null`
+- `setSelectedLibrary(id: number | null): void`
+- `getLibraries(): Promise`
+
+**Step 3: Library filter dropdown component** (NEW file `library-filter.ts`)
+
+Create `frontend/src/components/library-filter/library-filter.ts` — a compact `` Lit component:
+- Uses `LibraryController` to get library list and current selection
+- Renders as a styled `
+
+ cd /mnt/vault/dev/golang/yellowjacket && wails build -tags webkit2_41
+
+ Library filter dropdown appears in top bar, shows "All Libraries" by default plus all configured libraries. Selecting a library causes all browse views (tracks, albums, artists, genres) to show only that library's content. Search respects the filter. Detail views (artist-details, genre-details) respect the filter. Playlists remain unfiltered. Scroll positions reset on filter change. Filter resets on app restart.
+
+
+
+ Task 2: Verify library filter, cross-library playlists, and phantom tracks end-to-end
+
+Run the app with `wails dev -tags webkit2_41` and verify all Phase 13 requirements:
+
+**1. Library filter dropdown (VIEW-02)**
+- [ ] Compact dropdown appears in top bar between title and search bar
+- [ ] Shows "All Libraries" as default selection
+- [ ] Lists all configured libraries by name
+- [ ] Selecting a library immediately refreshes all views
+
+**2. Unified view — All Libraries (VIEW-01)**
+- [ ] With "All Libraries" selected, track list shows tracks from ALL libraries
+- [ ] Albums view shows albums from all libraries
+- [ ] Artists view shows artists from all libraries
+- [ ] Genres view shows genres from all libraries
+
+**3. Filtered view — specific library (VIEW-02, VIEW-03)**
+- [ ] Selecting a specific library shows only that library's tracks
+- [ ] Albums view shows only albums with tracks in selected library
+- [ ] Artists view shows only artists with albums in selected library
+- [ ] Genres view shows only genres with tracks in selected library
+- [ ] Artist detail page (click an artist) shows only that artist's albums in selected library
+- [ ] Genre detail page (click a genre) shows only that genre's tracks in selected library
+
+**4. Search with library filter (VIEW-04)**
+- [ ] With "All Libraries" selected, search returns results from all libraries
+- [ ] With a specific library selected, search returns only matches from that library
+
+**5. Cross-library playlists (PLAY-01)**
+- [ ] Create a playlist and add tracks from different libraries — they all appear correctly
+- [ ] Playlist view is NOT affected by library filter (shows all playlists always)
+- [ ] Playlist detail view shows ALL tracks regardless of active library filter
+
+**6. Phantom tracks (PLAY-02, PLAY-03)**
+- [ ] Remove a library that has tracks in a playlist
+- [ ] Those tracks become phantom entries (greyed out with warning icon)
+- [ ] Phantom tracks show preserved title, artist, album metadata
+- [ ] Phantom resolver (locate/remove buttons) works on the phantom entries
+
+**7. UX details**
+- [ ] Scroll positions reset when switching library filter
+- [ ] Loading skeleton shows briefly during filter switch
+- [ ] Filter resets to "All Libraries" on app restart
+- [ ] Queue plays correctly when tracks are from filtered view
+
+ Human verification — all checklist items above pass
+ All 7 requirement groups verified: VIEW-01 (unified), VIEW-02 (filtered), VIEW-03 (browse views filtered), VIEW-04 (search filtered), PLAY-01 (cross-library playlists), PLAY-02 (phantom preservation), PLAY-03 (phantom display)
+
+
+
+
+
+- `wails build -tags webkit2_41` completes successfully
+- Library filter dropdown renders in top bar
+- All 7 requirements verified: VIEW-01 (unified), VIEW-02 (filtered), VIEW-03 (browse filtered), VIEW-04 (search filtered), PLAY-01 (cross-library playlists), PLAY-02 (phantom preservation), PLAY-03 (phantom display)
+- No regression in existing functionality (playlists, queue, playback)
+
+
+
+- Library filter dropdown in top bar with "All Libraries" default + per-library options
+- Track, album, artist, genre views all filter by selected library
+- Search respects active library filter
+- Playlists remain unfiltered (cross-library by design)
+- Phantom tracks display correctly after library removal
+- Queue populated from filtered context
+- Human checkpoint passed
+
+
+