diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 5496a62..865a8e0 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -92,7 +92,10 @@ Plans:
3. User can remove a library — its tracks are deleted, shared artists/albums/genres used only by that library are cleaned up, but entities shared with other libraries survive intact
4. Removing a library cleans up FTS5 search index entries for that library's tracks (no stale search results)
5. Queue tracks from a removed library are cascade-deleted; the queue continues playing from the next valid track
-**Plans:** TBD
+**Plans:** 2 plans
+Plans:
+- [ ] 12-01-PLAN.md — Backend CRUD API + orphan cleanup + queue compaction + events
+- [ ] 12-02-PLAN.md — Frontend library management UI in settings + sidebar cleanup
### Phase 13: Library Views & Phantom Tracks
**Goal:** Users experience a unified multi-library presentation with optional filtering and graceful playlist preservation
diff --git a/.planning/phases/12-library-crud-data-integrity/12-01-PLAN.md b/.planning/phases/12-library-crud-data-integrity/12-01-PLAN.md
new file mode 100644
index 0000000..bceae36
--- /dev/null
+++ b/.planning/phases/12-library-crud-data-integrity/12-01-PLAN.md
@@ -0,0 +1,408 @@
+---
+phase: 12-library-crud-data-integrity
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/library/crud.go
+ - backend/events/events.go
+ - frontend/src/events.ts
+ - backend/queue/queue.go
+autonomous: true
+requirements: [LIB-01, LIB-02, LIB-03, DATA-02, DATA-03, PLAY-04]
+
+must_haves:
+ truths:
+ - "AddLibrary creates a library row, emits LibraryAdded event, and triggers ScanLibrary"
+ - "RenameLibrary validates uniqueness and length, updates name, emits LibraryRenamed event"
+ - "RemoveLibrary atomically deletes tracks, populates phantom metadata on playlist_tracks, deletes orphaned entities, deletes the library row, rebuilds FTS5 index, and emits LibraryRemoved event"
+ - "Orphan cleanup correctly handles the dual artist_credit FK (recordings + release_groups)"
+ - "Queue tracks from a removed library are cascade-deleted and queue state is compacted"
+ - "Currently-playing track from a removed library causes playback to stop before removal proceeds"
+ artifacts:
+ - path: "backend/library/crud.go"
+ provides: "AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact methods"
+ exports: ["AddLibrary", "RenameLibrary", "RemoveLibrary", "GetRemovalImpact", "RemovalSummary", "RemovalImpact"]
+ - path: "backend/events/events.go"
+ provides: "LibraryAdded, LibraryRenamed, LibraryRemoved event constants"
+ contains: "LibraryAdded"
+ - path: "frontend/src/events.ts"
+ provides: "Regenerated event constants"
+ contains: "LibraryAdded"
+ key_links:
+ - from: "backend/library/crud.go"
+ to: "backend/library/scan_queue.go"
+ via: "ScanLibrary call after AddLibrary"
+ pattern: "l\\.ScanLibrary"
+ - from: "backend/library/crud.go"
+ to: "backend/database/search.go"
+ via: "RebuildSearchIndex after removal"
+ pattern: "RebuildSearchIndex"
+ - from: "backend/library/crud.go"
+ to: "backend/queue/queue.go"
+ via: "Queue compaction after cascade delete"
+ pattern: "CompactAfterLibraryRemoval"
+---
+
+
+Implement the backend Library CRUD API (AddLibrary, RenameLibrary, RemoveLibrary) with full data integrity: orphan cleanup, phantom track conversion, FTS5 rebuild, queue compaction, and event emission.
+
+Purpose: This is the core backend for Phase 12 — all frontend library management UI depends on these Wails-bound methods.
+Output: `backend/library/crud.go` with all CRUD methods, updated events, queue compaction method.
+
+
+
+@/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/12-library-crud-data-integrity/12-RESEARCH.md
+@.planning/phases/12-library-crud-data-integrity/12-CONTEXT.md
+@.planning/phases/11-per-library-scan-pipeline/11-01-SUMMARY.md
+@.planning/phases/10-schema-migration/10-01-SUMMARY.md
+
+@backend/library/library.go
+@backend/library/scan_queue.go
+@backend/library/rescan.go
+@backend/library/query.go
+@backend/events/events.go
+@backend/database/search.go
+@backend/queue/queue.go
+@backend/database/sql/queries/libraries.sql
+@backend/database/sql/schemas/_libraries.sql
+@backend/database/sql/schemas/audio_files.sql
+@backend/database/sql/schemas/playlist_tracks.sql
+@backend/player/player.go
+
+
+
+
+From backend/library/scan_queue.go:
+```go
+func (l *Library) ScanLibrary(id int64) error
+func (l *Library) ScanAllLibraries() error
+func (l *Library) CancelCurrentScan()
+func (l *Library) CancelAllScans()
+```
+
+From backend/library/library.go:
+```go
+type Library struct {
+ ctx context.Context
+ db *database.DB
+ conf *config.Config
+ logger *slog.Logger
+ // ... scan state fields, mu sync.Mutex
+}
+```
+
+From backend/database/search.go:
+```go
+func (d *DB) RebuildSearchIndex() error
+```
+
+From backend/database/sql/queries/libraries.sql:
+```sql
+-- name: CreateLibrary :one
+INSERT INTO libraries (name, path) VALUES (?, ?) RETURNING *;
+-- name: GetLibrary :one
+SELECT * FROM libraries WHERE id = ? LIMIT 1;
+-- name: GetLibraryByPath :one
+SELECT * FROM libraries WHERE path = ? LIMIT 1;
+-- name: GetAllLibraries :many
+SELECT * FROM libraries ORDER BY name;
+-- name: UpdateLibraryName :exec
+UPDATE libraries SET name = ? WHERE id = ?;
+-- name: DeleteLibrary :exec
+DELETE FROM libraries WHERE id = ?;
+-- name: CountLibraries :one
+SELECT COUNT(*) AS count FROM libraries;
+-- name: CountAudioFilesByLibrary :one
+SELECT COUNT(*) AS count FROM audio_files WHERE library_id = ?;
+```
+
+From backend/queue/queue.go:
+```go
+func (q *Queue) Clear()
+func (q *Queue) EmitCurrentState()
+func (q *Queue) GetState() State
+type TrackLoader interface {
+ IsPlaying() bool
+ CurrentPositionSeconds() (int, error)
+ UnloadTrack()
+}
+```
+
+From backend/events/events.go:
+```go
+// Library events.
+const (
+ LibraryScanStarted = "LibraryScanStarted"
+ LibraryScanProgress = "LibraryScanProgress"
+ LibraryScanComplete = "LibraryScanComplete"
+)
+```
+
+From backend/player/player.go:
+```go
+func (p *Player) IsPlaying() bool
+func (p *Player) UnloadTrack()
+```
+
+
+
+
+
+
+ Task 1: Implement Library CRUD methods and orphan cleanup pipeline
+
+ backend/library/crud.go
+ backend/events/events.go
+ frontend/src/events.ts
+
+
+Create `backend/library/crud.go` with the following methods on the `Library` struct:
+
+**Types:**
+```go
+// RemovalImpact contains pre-removal counts for the confirmation dialog.
+type RemovalImpact struct {
+ TrackCount int64 `json:"trackCount"`
+ PlaylistsAffected int64 `json:"playlistsAffected"`
+ QueueItemCount int64 `json:"queueItemCount"`
+}
+
+// RemovalSummary contains post-removal counts for the toast notification.
+type RemovalSummary struct {
+ TracksDeleted int64 `json:"tracksDeleted"`
+ ArtistsRemoved int64 `json:"artistsRemoved"`
+ AlbumsRemoved int64 `json:"albumsRemoved"`
+ GenresRemoved int64 `json:"genresRemoved"`
+ PlaylistsAffected int64 `json:"playlistsAffected"`
+ QueueItemsRemoved int64 `json:"queueItemsRemoved"`
+}
+```
+
+**AddLibrary(path string) (\*sqlcgen.Library, error):**
+- Validate path exists with `os.Stat`
+- Auto-name from `filepath.Base(path)`
+- Call `l.db.Queries.CreateLibrary(l.ctx, ...)` (the path UNIQUE constraint prevents duplicate paths)
+- Emit `events.LibraryAdded` event with the library struct
+- Start scanning async: `go func() { l.ScanLibrary(lib.ID) }()` — log error if it fails
+- Return the created library
+
+**RenameLibrary(id int64, newName string) error:**
+- Trim and validate: 1-50 chars, non-empty
+- Check uniqueness: call `GetAllLibraries`, iterate to find conflicting name (excluding self). Use application-level validation per research recommendation (no schema migration needed).
+- Call `l.db.Queries.UpdateLibraryName(l.ctx, ...)`
+- Emit `events.LibraryRenamed` with `map[string]any{"id": id, "name": newName}`
+
+**GetRemovalImpact(libraryID int64) (\*RemovalImpact, error):**
+- Three read-only queries (all hand-crafted SQL with SAFETY comments):
+ - Track count: `SELECT COUNT(*) FROM audio_files WHERE library_id = ?`
+ - Playlists affected: `SELECT COUNT(DISTINCT pt.playlist_id) FROM playlist_tracks pt JOIN audio_files af ON pt.audio_file_id = af.id WHERE af.library_id = ?`
+ - Queue items: `SELECT COUNT(*) FROM queue_tracks qt JOIN audio_files af ON qt.audio_file_id = af.id WHERE af.library_id = ?`
+
+**RemoveLibrary(id int64) (\*RemovalSummary, error):**
+This is the critical method. Follow the exact order from RESEARCH.md to avoid the phantom metadata pitfall:
+
+1. **Cancel active scan** — If this library is currently scanning, cancel it and remove from queue. Call `l.cancelLibraryScan(id)` (new unexported helper that checks `l.currentScanLibraryID` and scan queue).
+2. **Stop playback if needed** — Check if the currently-playing track belongs to this library via a query: `SELECT COUNT(*) FROM audio_files WHERE library_id = ? AND file_path = ?` where the file_path comes from `l.player.GetCurrentFilePath()`. Need to expose a way to check — add a `currentTrackBelongsToLibrary` helper that uses the Queue to get the current track's file path and checks it against the library. If it matches, call `l.player.UnloadTrack()`.
+3. **Pre-count** for summary (track count, queue items affected, playlists affected).
+4. **Begin transaction** — `l.db.DB().BeginTx(l.ctx, nil)`
+5. **Populate phantom metadata** — MUST run BEFORE delete. Hand-crafted SQL UPDATE that copies live track metadata into phantom columns on playlist_tracks for tracks belonging to this library. See 12-RESEARCH.md Pattern 3 for the exact SQL.
+6. **Delete audio_files** — `DELETE FROM audio_files WHERE library_id = ?`. This triggers CASCADE on queue_tracks and SET NULL on playlist_tracks.audio_file_id.
+7. **Delete orphaned recordings** — `DELETE FROM recordings WHERE id NOT IN (SELECT DISTINCT recording_id FROM audio_files)`
+8. **Delete orphaned recording_genres** — `DELETE FROM recording_genres WHERE recording_id NOT IN (SELECT id FROM recordings)`
+9. **Delete orphaned release_group_recordings** — `DELETE FROM release_group_recordings WHERE recording_id NOT IN (SELECT id FROM recordings)`
+10. **Delete orphaned release_groups** — `DELETE FROM release_groups WHERE id NOT IN (SELECT DISTINCT release_group_id FROM release_group_recordings)`
+11. **Delete orphaned artist_credits** — CRITICAL: check BOTH recordings AND release_groups: `DELETE FROM artist_credit WHERE id NOT IN (SELECT DISTINCT artist_credit_id FROM recordings) AND id NOT IN (SELECT DISTINCT album_artist_credit_id FROM release_groups WHERE album_artist_credit_id IS NOT NULL)`
+12. **Delete orphaned artist_credit_artists** — `DELETE FROM artist_credit_artist WHERE credit_id NOT IN (SELECT id FROM artist_credit)`
+13. **Delete orphaned artists** — `DELETE FROM artists WHERE id NOT IN (SELECT DISTINCT artist_id FROM artist_credit_artist)`
+14. **Delete orphaned genres** — `DELETE FROM genres WHERE id NOT IN (SELECT DISTINCT genre_id FROM recording_genres)`
+15. **Collect orphaned cover_art file paths** — `SELECT file_path FROM cover_art WHERE id NOT IN (SELECT DISTINCT cover_art_id FROM release_groups WHERE cover_art_id IS NOT NULL)` — store in a slice for post-commit cleanup.
+16. **Delete orphaned cover_art rows** — `DELETE FROM cover_art WHERE id NOT IN (SELECT DISTINCT cover_art_id FROM release_groups WHERE cover_art_id IS NOT NULL)`
+17. **Delete library row** — `DELETE FROM libraries WHERE id = ?`
+18. **Commit transaction**
+19. **Post-commit: Rebuild FTS5** — `l.db.RebuildSearchIndex()` (cannot run inside transaction)
+20. **Post-commit: Delete orphaned cover art files** — iterate collected paths, `os.Remove()`, log warnings on failure
+21. **Post-commit: Compact queue** — Call the new `l.queue.CompactAfterLibraryRemoval()` method (see Task 2)
+22. **Emit events** — `events.LibraryRemoved` with `map[string]any{"id": id, "summary": summary}`
+23. **Return summary**
+
+All hand-crafted SQL statements MUST have SAFETY comments following the project convention: `// SAFETY: [reason sqlc can't handle] + [safety assurance]`.
+
+**cancelLibraryScan(id int64):**
+Unexported helper. Check if `l.currentScanLibraryID` matches `id` — if so, call `CancelCurrentScan()`. Also remove the library from the scan queue slice (filter it out under `l.scanMu` lock).
+
+**currentTrackBelongsToLibrary(libraryID int64) bool:**
+Unexported helper. Get the current track file path from the queue (need to check if queue has a method to expose this, or query via `q.GetState().Tracks[q.GetState().CurrentIndex].FilePath`). Then query `SELECT library_id FROM audio_files WHERE file_path = ?` and compare.
+
+Actually — for stopping playback: the Library struct doesn't directly hold a reference to Player. Use the existing `RescanHooks.PreClear` pattern or add a `StopPlaybackHook func()` field on Library. In `app.go` OnStartup, wire it:
+```go
+yj.library.StopPlaybackHook = func() {
+ yj.player.UnloadTrack()
+}
+```
+But that's for stopping unconditionally. For checking if the current track belongs to a library, it's simpler to do the check inside `RemoveLibrary` via a hand-crafted query: `SELECT COUNT(*) FROM audio_files af JOIN queue_tracks qt ON qt.audio_file_id = af.id WHERE af.library_id = ? AND qt.position = (SELECT current_position FROM queue LIMIT 1)`. If count > 0, call the hook.
+
+Better approach: add two fields to Library:
+```go
+// StopPlaybackForLibrary is called before library removal if the
+// currently-playing track belongs to the library being removed.
+// Wired in app.go OnStartup.
+StopPlaybackForLibrary func()
+// GetQueueState returns the current queue state for library removal checks.
+// Wired in app.go OnStartup.
+GetQueueState func() (currentFilePath string, ok bool)
+```
+
+Actually, the simplest approach that follows existing patterns: Library already has a `rescanHooks RescanHooks` field. Add a new field:
+```go
+removalHooks struct {
+ stopPlayback func()
+ compactQueue func()
+}
+```
+Wire in app.go:
+```go
+yj.library.SetRemovalHooks(library.RemovalHooks{
+ StopPlayback: func() { yj.player.UnloadTrack() },
+ CompactQueue: func() { yj.queue.CompactAfterLibraryRemoval() },
+})
+```
+Then for the "does current track belong to this library" check, just use a DB query in the transaction-preparation stage.
+
+**Add to events.go:**
+```go
+// Library CRUD events.
+const (
+ LibraryAdded = "LibraryAdded"
+ LibraryRenamed = "LibraryRenamed"
+ LibraryRemoved = "LibraryRemoved"
+)
+```
+
+Then run `go generate ./backend/events/...` to regenerate `frontend/src/events.ts`.
+
+Use the SAFETY comment convention for ALL hand-crafted SQL (every ExecContext/QueryContext/QueryRowContext call).
+Follow error sentinel convention (err113): define `var errLibraryNameEmpty`, `var errLibraryNameTooLong`, `var errLibraryNameDuplicate`, `var errLibraryPathNotExist` as package-level vars.
+Follow nlreturn convention: blank line after early return blocks.
+Follow godot convention: doc comments end with periods.
+Follow wsl convention: blank line before var/const declarations.
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && go build ./backend/... && go vet ./backend/library/... && golangci-lint run ./backend/library/crud.go ./backend/events/events.go
+
+
+ - crud.go exists with AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact, cancelLibraryScan methods
+ - All hand-crafted SQL has SAFETY comments
+ - RemoveLibrary follows exact order: phantom populate → delete audio_files → orphan cleanup → delete library → commit → FTS5 rebuild → cover art file cleanup → queue compact → events
+ - events.go has LibraryAdded, LibraryRenamed, LibraryRemoved constants
+ - events.ts is regenerated
+ - `go build ./backend/...` passes
+
+
+
+
+ Task 2: Add queue compaction method and wire removal hooks in app.go
+
+ backend/queue/queue.go
+ backend/app.go
+ backend/library/crud.go
+
+
+**Queue compaction method** — Add to `backend/queue/queue.go`:
+
+```go
+// CompactAfterLibraryRemoval reloads queue state from the database
+// after a library removal has cascade-deleted queue_tracks rows.
+// It resets currentIndex to 0 (or -1 if empty), clears shuffleOrder,
+// unloads the current track if it was removed, and emits QueueChanged.
+func (q *Queue) CompactAfterLibraryRemoval() {
+```
+
+Implementation:
+1. Acquire `q.mu`
+2. Call `q.db.Queries.GetQueueTracks(q.db.Ctx)` to get the surviving queue tracks from DB
+3. Rebuild `q.tracks` from the DB rows
+4. If the previous current track's file path is no longer in the new track list:
+ - Set `q.currentIndex = 0` (or -1 if empty)
+ - Call `q.player.UnloadTrack()` if player is set
+5. Else: find the current track in the new list and update `q.currentIndex`
+6. Clear `q.shuffleOrder = nil` (will be regenerated on next shuffle toggle)
+7. Call `q.commitMutation(false)` to persist the compacted state
+8. Call `q.emitQueueChanged()` to push update to frontend
+
+Need to check if `GetQueueTracks` query exists. If not, the queue persistence uses its own reload pattern. Check `backend/queue/persistence.go` for the restore pattern and reuse it. The key point is that cascade DELETE already removed the rows from `queue_tracks` — we just need to reload and reindex.
+
+**Wire removal hooks in app.go** — In `OnStartup`, after existing hook wiring, add:
+
+```go
+yj.library.SetRemovalHooks(library.RemovalHooks{
+ StopPlayback: func() { yj.player.UnloadTrack() },
+ CompactQueue: func() { yj.queue.CompactAfterLibraryRemoval() },
+})
+```
+
+**Add RemovalHooks type to crud.go** (or library.go):
+
+```go
+// RemovalHooks contains callbacks invoked during library removal.
+// These break circular dependencies between library, player, and queue packages.
+type RemovalHooks struct {
+ // StopPlayback stops the currently-playing track.
+ StopPlayback func()
+ // CompactQueue reloads queue state after cascade deletes.
+ CompactQueue func()
+}
+
+func (l *Library) SetRemovalHooks(h RemovalHooks) {
+ l.removalHooks = h
+}
+```
+
+Add `removalHooks RemovalHooks` field to the Library struct in library.go.
+
+Make sure RemoveLibrary in crud.go calls these hooks at the appropriate points (StopPlayback before the transaction if current track belongs to the library, CompactQueue after the transaction commits).
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && go build ./... && go vet ./backend/queue/... ./backend/library/... && golangci-lint run ./backend/queue/queue.go ./backend/app.go
+
+
+ - CompactAfterLibraryRemoval method exists on Queue
+ - RemovalHooks type exists with StopPlayback and CompactQueue callbacks
+ - app.go wires removal hooks in OnStartup
+ - Library struct has removalHooks field
+ - `go build ./...` passes (full build including frontend binding generation)
+
+
+
+
+
+
+1. `go build ./...` — full project builds with no errors
+2. `go vet ./backend/...` — no vet issues
+3. `golangci-lint run ./backend/library/ ./backend/queue/ ./backend/events/` — no lint issues
+4. `go test ./backend/database/... -count=1` — existing database tests still pass
+5. `go test ./backend/queue/... -count=1` — existing queue tests still pass
+6. `go test ./backend/library/... -count=1` — existing library tests still pass
+7. Verify events.ts was regenerated with new event constants
+
+
+
+- All four CRUD methods (AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact) are implemented and compile
+- RemoveLibrary follows the correct order: phantom populate → delete → orphan cleanup → commit → FTS5 rebuild
+- Queue compaction handles cascade-deleted tracks correctly
+- All events (LibraryAdded, LibraryRenamed, LibraryRemoved) are defined and auto-generated to frontend
+- Existing tests pass with no regressions
+
+
+
diff --git a/.planning/phases/12-library-crud-data-integrity/12-02-PLAN.md b/.planning/phases/12-library-crud-data-integrity/12-02-PLAN.md
new file mode 100644
index 0000000..754ea9f
--- /dev/null
+++ b/.planning/phases/12-library-crud-data-integrity/12-02-PLAN.md
@@ -0,0 +1,290 @@
+---
+phase: 12-library-crud-data-integrity
+plan: 02
+type: execute
+wave: 2
+depends_on: [12-01]
+files_modified:
+ - frontend/src/components/config-page/config-page.ts
+ - frontend/src/components/sidebar/app-sidebar.ts
+ - frontend/index.ts
+autonomous: false
+requirements: [LIB-01, LIB-02, LIB-03, LIB-06]
+
+must_haves:
+ truths:
+ - "User sees a library list in the settings page showing name, path, and track count for each library"
+ - "User can click 'Add Library' to open a folder picker, library auto-names from folder and scan starts"
+ - "User can rename a library inline (click name or overflow menu) with Enter to save, Escape to cancel"
+ - "User sees a confirmation dialog with real impact counts before library removal"
+ - "User sees a toast notification with removal summary after library is removed"
+ - "The sidebar no longer has a 'Libraries' navigation item"
+ artifacts:
+ - path: "frontend/src/components/config-page/config-page.ts"
+ provides: "Library management section with list, add, rename, remove, toast"
+ contains: "renderLibraryList"
+ - path: "frontend/src/components/sidebar/app-sidebar.ts"
+ provides: "Sidebar without 'libraries' nav item"
+ - path: "frontend/index.ts"
+ provides: "No 'libraries' view case in router"
+ key_links:
+ - from: "frontend/src/components/config-page/config-page.ts"
+ to: "@go/library/Library"
+ via: "Wails bindings for AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact"
+ pattern: "AddLibrary|RenameLibrary|RemoveLibrary|GetRemovalImpact"
+ - from: "frontend/src/components/config-page/config-page.ts"
+ to: "frontend/src/events.ts"
+ via: "EventsOn for LibraryAdded, LibraryRenamed, LibraryRemoved"
+ pattern: "Events\\.Library(Added|Renamed|Removed)"
+---
+
+
+Replace the config-page library section with a full library management UI: library list with track counts, Add Library button with folder picker, inline rename, remove with impact dialog and toast, overflow menus. Remove sidebar "Libraries" nav item and its view routing.
+
+Purpose: Users can manage their music libraries entirely from the settings page per user decisions.
+Output: Updated config-page with library CRUD UI, cleaned-up sidebar and router.
+
+
+
+@/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/12-library-crud-data-integrity/12-RESEARCH.md
+@.planning/phases/12-library-crud-data-integrity/12-CONTEXT.md
+@.planning/phases/12-library-crud-data-integrity/12-01-SUMMARY.md
+
+@frontend/src/components/config-page/config-page.ts
+@frontend/src/components/sidebar/app-sidebar.ts
+@frontend/src/components/library-manager/library-manager.ts
+@frontend/index.ts
+@frontend/src/store/library-store.ts
+@frontend/src/events.ts
+
+
+
+
+From backend/library/crud.go (via Wails auto-generated bindings):
+```typescript
+// @go/library/Library
+export function AddLibrary(path: string): Promise;
+export function RenameLibrary(id: number, newName: string): Promise;
+export function RemoveLibrary(id: number): Promise;
+export function GetRemovalImpact(id: number): Promise;
+```
+
+From backend/library/query.go (existing bindings):
+```typescript
+export function GetAllLibraries(): Promise; // via database queries
+```
+
+From backend/database/sql/queries/libraries.sql (existing):
+```typescript
+// GetAllLibraries returns [{id, name, path, created_at}]
+// CountAudioFilesByLibrary returns {count}
+```
+
+From frontend/src/events.ts (regenerated in Plan 01):
+```typescript
+export const Events = {
+ // ...existing events...
+ LibraryAdded: "LibraryAdded",
+ LibraryRenamed: "LibraryRenamed",
+ LibraryRemoved: "LibraryRemoved",
+} as const;
+```
+
+From frontend/src/components/config-page/config-page.ts (existing patterns):
+```typescript
+// ConfigPage uses @state() decorators for reactive state
+// renderXxxSection() methods for each settings section
+// EventsOn() in connectedCallback for event subscriptions
+// config-field component for form fields
+// Scan state tracking: scanning, scanPaused, scanProgress, etc.
+```
+
+
+
+
+
+
+ Task 1: Replace config-page library section with library management UI
+
+ frontend/src/components/config-page/config-page.ts
+
+
+Replace the existing `renderLibrarySection()` method in config-page.ts with a full library management UI. The section currently shows a single directory path field + rescan button. Replace it with:
+
+**New state properties (add to class):**
+```typescript
+@state() private libraries: Array<{id: number; name: string; path: string; trackCount: number}> = [];
+@state() private editingLibraryId: number | null = null;
+@state() private editingName: string = '';
+@state() private removingLibraryId: number | null = null;
+@state() private removalImpact: {trackCount: number; playlistsAffected: number; queueItemCount: number} | null = null;
+@state() private isRemoving: boolean = false;
+@state() private toastMessage: string = '';
+@state() private toastVisible: boolean = false;
+@state() private activeMenuId: number | null = null;
+```
+
+**Load library list:**
+- In `connectedCallback` (or existing initialization), call `GetAllLibraries()` from Wails bindings, then for each library call `CountAudioFilesByLibrary(lib.id)` to get track counts (or add a new Go method that returns libraries with counts — but simpler to loop since there are typically 1-5 libraries).
+- Actually, better approach: Create a `loadLibraries()` method that calls `GetAllLibraries()` and maps results, enriching each with a `CountAudioFilesByLibrary` call. Store in `this.libraries`.
+- Call `loadLibraries()` on connectedCallback and after any CRUD event.
+
+**Event subscriptions (add to connectedCallback):**
+```typescript
+EventsOn(Events.LibraryAdded, () => this.loadLibraries());
+EventsOn(Events.LibraryRenamed, () => this.loadLibraries());
+EventsOn(Events.LibraryRemoved, () => this.loadLibraries());
+```
+
+**Remove old library config state:**
+Remove the `directoryPath` state property, `loadLibraryConfig()` method, `GetLibraryDirectory` and `SetLibraryDirectory` imports (these are legacy single-directory methods). Remove the old `config-field` for Library Directory.
+
+**renderLibrarySection() — complete replacement:**
+The section heading should be "Libraries" (not "Library"). Use ``.
+
+Content:
+1. **Library list** — For each library in `this.libraries`, render a row:
+ - If `this.editingLibraryId === lib.id`: render an input field with the editing name, Enter to save (call `RenameLibrary`), Escape to cancel
+ - Else: render `${lib.name}`, `${lib.path}`, `${lib.trackCount} tracks`, and an overflow `...` button
+ - The overflow button toggles `this.activeMenuId` — when active, shows a dropdown with: Rename, Rescan, Remove
+ - Rename: sets `this.editingLibraryId = lib.id; this.editingName = lib.name`
+ - Rescan: calls `ScanLibrary(lib.id)` from existing Wails bindings
+ - Remove: calls `GetRemovalImpact(lib.id)`, stores result in `this.removalImpact`, sets `this.removingLibraryId = lib.id` to show the confirmation dialog
+ - Click outside overflow menu closes it (add a document click listener)
+
+2. **Add Library button** — Below the list:
+ ```html
+
+ ```
+ `handleAddLibrary`: Call `DirectoryPicker()` from `@go/frontendutil/FrontendUtil`. If user selects a path, call `AddLibrary(path)`. The backend auto-names from folder name and triggers scan.
+
+3. **Removal confirmation dialog** — Shown when `this.removingLibraryId !== null`:
+ - Overlay with dialog box (same pattern as cancel scan dialog in library-manager.ts)
+ - Title: "Remove Library"
+ - Message: `Remove '${libraryName}'? This will delete ${impact.trackCount} tracks, affect ${impact.playlistsAffected} playlists, and remove ${impact.queueItemCount} queue items.`
+ - Two buttons: "Cancel" (closes dialog) and "Remove" (calls `RemoveLibrary(id)`)
+ - When "Remove" is clicked: set `this.isRemoving = true` to show a spinner. On completion: close dialog, show toast with summary, reload libraries.
+
+4. **Toast notification** — A simple div at the bottom of the component:
+ ```html
+ ${this.toastVisible ? html`
${this.toastMessage}
` : nothing}
+ ```
+ `showToast(message: string)` method: sets `this.toastMessage`, `this.toastVisible = true`, then `setTimeout(() => this.toastVisible = false, 4000)`.
+ After successful removal: `this.showToast("Removed '${name}' (${summary.tracksDeleted} tracks deleted)")`.
+
+**Scan actions integration:**
+Keep the existing scan actions (Soft Scan, Full Rescan, Scan All Libraries, Pause, Resume, Cancel) below the library list — they operate on the currently scanning library. The progress bar and scan status remain unchanged.
+
+Remove the old library directory `config-field` and `SetLibraryDirectory` logic entirely.
+
+**Styling (add to static styles):**
+- `.library-list` — flex column with gap
+- `.library-row` — flex row with items center, padding, border-bottom, hover state
+- `.library-name` — flex: 1, clickable for rename
+- `.library-path` — color: dimmed, font-size smaller, truncate with ellipsis
+- `.library-count` — color: dimmed
+- `.overflow-btn` — cursor pointer, no border, background transparent, letter-spacing for "···"
+- `.overflow-menu` — absolute position, background surface, border, shadow, z-index, list items with hover
+- `.edit-input` — styled text input for inline rename
+- `.removal-dialog-overlay` — fixed full screen, background semi-transparent
+- `.removal-dialog` — centered box, background surface, padding, rounded corners
+- `.toast` — fixed bottom center, background surface, padding, border-radius, box-shadow, animation (fade in/out via CSS transition on opacity)
+- `.spinner` — simple CSS spinner (border animation)
+
+Use design tokens where applicable (--yj-text-sm for paths/counts, etc.).
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && npx tsc --noEmit
+
+
+ - Config-page shows library list with name, path, track count per library
+ - Add Library button opens folder picker and creates library
+ - Inline rename with Enter/Escape works
+ - Overflow menu shows Rename, Rescan, Remove actions
+ - Removal dialog shows real impact counts
+ - Toast notification shows after removal
+ - Old single-directory library config UI is removed
+ - TypeScript compiles with no errors
+
+
+
+
+ Task 2: Remove Libraries sidebar nav item and view routing
+
+ frontend/src/components/sidebar/app-sidebar.ts
+ frontend/index.ts
+
+
+Per user decision: "Remove the libraries tab from the sidebar list entirely."
+
+**app-sidebar.ts:**
+1. Remove `'libraries'` from the `View` type union: change `'home' | 'libraries' | 'playlists' | ...` to `'home' | 'playlists' | ...`
+2. Remove the `{ id: 'libraries', label: 'Libraries', icon: 'folder-open' }` entry from the nav items array
+
+**index.ts:**
+1. Remove the `case 'libraries':` block that sets `mainContent.innerHTML = ''`
+2. Remove the `import '@components/library-manager/library-manager.ts'` import (the component is no longer used)
+
+Note: Do NOT delete the `library-manager.ts` file itself — it may still be referenced elsewhere or useful for reference. Just remove its import and routing.
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && npx tsc --noEmit
+
+
+ - Sidebar does not show "Libraries" nav item
+ - Clicking where Libraries was no longer routes to library-manager view
+ - library-manager component import removed from index.ts
+ - TypeScript compiles with no errors
+
+
+
+
+ Task 3: Verify library management UI end-to-end
+ frontend/src/components/config-page/config-page.ts
+
+Human verification of the complete library management UI.
+
+Launch the app with `wails dev` and verify:
+1. Navigate to Settings — "Libraries" section shows existing library with name, path, and track count
+2. Click "Add Library" — folder picker opens. Select a folder with music. Library appears in list and scan starts.
+3. Click `...` overflow menu — Rename, Rescan, Remove options appear
+4. Click Rename — name becomes editable. Type new name, press Enter. Name updates.
+5. Press Escape while editing — rename is cancelled
+6. Click Remove on a test library — confirmation dialog shows real impact counts
+7. Click Remove in dialog — spinner shows, then toast notification with removal summary
+8. Sidebar no longer has "Libraries" nav item
+9. Scan controls (Soft Scan, Full Rescan, Scan All, Pause, Cancel) still work
+
+ Manual verification — all 9 checks pass
+ Library management UI works end-to-end: add, rename, remove with correct data lifecycle
+
+
+
+
+
+1. `npx tsc --noEmit` — TypeScript compiles with no errors
+2. `wails dev` — app launches without errors
+3. Library list shows in settings with correct data
+4. Add/rename/remove flows work end-to-end
+5. Sidebar has no "Libraries" item
+6. Scan controls still function
+
+
+
+- Library management UI replaces old single-directory config in settings page
+- All CRUD operations work: add (with folder picker + auto-scan), rename (inline edit), remove (with confirmation + toast)
+- Sidebar "Libraries" nav item is removed
+- No TypeScript compilation errors
+
+
+