--- 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 `