diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index c73155a..0cf7aa1 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -76,7 +76,11 @@ Plans:
2. Only one library scans at a time — requesting a second scan while one is running either queues it or is rejected with clear feedback
3. Scan progress UI identifies which library is currently being scanned (library name visible in progress indicator)
4. Existing cancel and pause/resume controls work correctly for per-library scans — cancelling one library's scan doesn't affect others
-**Plans:** TBD
+**Plans:** 3 plans
+Plans:
+- [ ] 11-01-PLAN.md — Backend scan queue coordinator, per-library scan methods, CreateAudioFile with library_id
+- [ ] 11-02-PLAN.md — Frontend progress UI with library name, cancel scope modal, Scan All button
+- [ ] 11-03-PLAN.md — App startup auto-scan wiring, legacy single-directory cleanup
### Phase 12: Library CRUD & Data Integrity
**Goal:** Users can add, rename, and remove libraries through the UI with correct data lifecycle management
diff --git a/.planning/phases/11-per-library-scan-pipeline/11-01-PLAN.md b/.planning/phases/11-per-library-scan-pipeline/11-01-PLAN.md
new file mode 100644
index 0000000..a4ccd47
--- /dev/null
+++ b/.planning/phases/11-per-library-scan-pipeline/11-01-PLAN.md
@@ -0,0 +1,358 @@
+---
+phase: 11-per-library-scan-pipeline
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/library/scan_queue.go
+ - backend/library/library.go
+ - backend/library/scan_control.go
+ - backend/library/config.go
+ - backend/library/rescan.go
+ - backend/library/metrics.go
+ - backend/events/events.go
+ - frontend/src/events.ts
+ - backend/database/sql/queries/audio_files.sql
+ - backend/database/sql/sqlcgen/audio_files.sql.go
+ - backend/database/sql/sqlcgen/models.go
+autonomous: true
+requirements: [LSCAN-01, LSCAN-02, LSCAN-04]
+
+must_haves:
+ truths:
+ - "ScanLibrary(id) scans only the directory associated with that library ID"
+ - "Only one library scans at a time — additional requests are silently queued"
+ - "Duplicate scan requests for the same library are silently ignored"
+ - "Cancel/pause/resume work per-library — cancelling one library starts the next queued"
+ - "Pausing freezes both the current scan AND the queue"
+ - "ScanAllLibraries queries all libraries and queues them sequentially"
+ artifacts:
+ - path: "backend/library/scan_queue.go"
+ provides: "Scan queue coordinator with sequential execution"
+ exports: ["ScanLibrary", "ScanAllLibraries", "CancelCurrentScan", "CancelAllScans"]
+ - path: "backend/library/library.go"
+ provides: "Updated Scan() accepting library ID and path"
+ - path: "backend/events/events.go"
+ provides: "Updated scan events with library identification"
+ - path: "backend/database/sql/queries/audio_files.sql"
+ provides: "CreateAudioFile with library_id parameter"
+ key_links:
+ - from: "backend/library/scan_queue.go"
+ to: "backend/library/library.go"
+ via: "scanQueue calls scanLibrary which calls internal scan pipeline"
+ pattern: "l\\.scanInternal"
+ - from: "backend/library/scan_queue.go"
+ to: "backend/database/sql/sqlcgen/libraries.sql.go"
+ via: "GetLibrary query to resolve library path from ID"
+ pattern: "Queries\\.GetLibrary"
+ - from: "backend/library/library.go"
+ to: "backend/database/sql/sqlcgen/audio_files.sql.go"
+ via: "CreateAudioFile now includes library_id"
+ pattern: "CreateAudioFileParams.*LibraryID"
+---
+
+
+Refactor the scan pipeline from scanning a single hardcoded directory to scanning individual libraries by database ID, with a sequential scan queue coordinator.
+
+Purpose: Enable per-library scanning (LSCAN-01), sequential coordination (LSCAN-02), and per-library cancel/pause scope (LSCAN-04) at the backend level.
+Output: `ScanLibrary(id)` and `ScanAllLibraries()` Wails-bound methods, scan queue coordinator, updated events with library identification, `CreateAudioFile` with `library_id`.
+
+
+
+@/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/11-per-library-scan-pipeline/11-CONTEXT.md
+@.planning/phases/09-scan-cancellation-keyboard-shortcuts/09-01-SUMMARY.md
+@.planning/phases/10-schema-migration/10-02-SUMMARY.md
+
+
+
+
+From backend/library/library.go:
+```go
+type Library struct {
+ mu sync.Mutex
+ ctx context.Context
+ logger *slog.Logger
+ conf *Config
+ db *database.DB
+ rescanHooks RescanHooks
+ scanActive bool
+ scanCancel context.CancelFunc
+ scanPaused bool
+ scanPauseCh chan struct{}
+}
+
+func (l *Library) Scan() (*ScanMetrics, error)
+func (l *Library) SetContext(ctx context.Context)
+func (l *Library) CancelScan()
+func (l *Library) PauseScan()
+func (l *Library) ResumeScan()
+func (l *Library) IsScanActive() bool
+func (l *Library) IsScanPaused() bool
+```
+
+From backend/library/config.go:
+```go
+type Config struct {
+ DirectoryPath Directory `toml:"DirectoryPath"`
+ ScanConcurrency ScanConcurrency `toml:"ScanConcurrency"`
+}
+```
+
+From backend/library/metrics.go:
+```go
+type ScanProgress struct {
+ Phase string `json:"phase"`
+ Total int64 `json:"total"`
+ Processed int64 `json:"processed"`
+ Added int64 `json:"added"`
+ Skipped int64 `json:"skipped"`
+ Updated int64 `json:"updated"`
+}
+
+type ScanMetrics struct { ... Cancelled bool ... }
+```
+
+From backend/events/events.go:
+```go
+const (
+ LibraryScanStarted = "LibraryScanStarted"
+ LibraryScanProgress = "LibraryScanProgress"
+ LibraryScanComplete = "LibraryScanComplete"
+ LibraryScanCancelled = "LibraryScanCancelled"
+ LibraryScanPaused = "LibraryScanPaused"
+ LibraryScanResumed = "LibraryScanResumed"
+)
+```
+
+From backend/database/sql/sqlcgen/libraries.sql.go:
+```go
+func (q *Queries) GetLibrary(ctx context.Context, id int64) (Library, error)
+func (q *Queries) GetAllLibraries(ctx context.Context) ([]Library, error)
+```
+
+From backend/database/sql/sqlcgen/audio_files.sql.go:
+```go
+type CreateAudioFileParams struct {
+ FilePath string
+ LengthMilliseconds int64
+ FileTypeID int64
+ RecordingID int64
+ SampleRate int64
+ BitDepth int64
+ Channels int64
+ Bitrate int64
+ FileSize int64
+ Basename string
+ // NOTE: library_id NOT included — uses DEFAULT 0
+}
+
+func (q *Queries) GetAudioFilesByLibrary(ctx context.Context, libraryID int64) ([]AudioFile, error)
+func (q *Queries) GetAllAudioFiles(ctx context.Context) ([]AudioFile, error)
+```
+
+
+
+
+
+
+ Task 1: Add library_id to CreateAudioFile + update events and progress types
+
+ backend/database/sql/queries/audio_files.sql
+ backend/database/sql/sqlcgen/audio_files.sql.go
+ backend/database/sql/sqlcgen/models.go
+ backend/events/events.go
+ frontend/src/events.ts
+ backend/library/metrics.go
+
+
+1. **Update CreateAudioFile SQL query** in `backend/database/sql/queries/audio_files.sql`:
+ - Add `library_id` to the INSERT column list and VALUES: `INSERT INTO audio_files (file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+ - This adds the `library_id` parameter so scans can associate files with their library.
+
+2. **Run `sqlc generate`** to regenerate Go code:
+ ```bash
+ sqlc generate
+ ```
+ This will update `CreateAudioFileParams` to include `LibraryID int64`.
+
+3. **Add new event constants** to `backend/events/events.go` — add a "Scan queue events" group:
+ ```go
+ // Scan queue events.
+ const (
+ LibraryScanQueued = "LibraryScanQueued"
+ LibraryScanQueueDrained = "LibraryScanQueueDrained"
+ )
+ ```
+
+4. **Regenerate TypeScript events** via `go generate ./backend/events/...` (uses the genevents tool).
+
+5. **Add library identification fields** to `ScanProgress` and `ScanMetrics` in `backend/library/metrics.go`:
+ - Add to `ScanProgress`: `LibraryID int64 \`json:"libraryId"\`` and `LibraryName string \`json:"libraryName"\``
+ - Add to `ScanProgress`: `QueuedCount int \`json:"queuedCount"\`` (number of libraries still queued after this one)
+ - Add to `ScanMetrics`: `LibraryID int64 \`json:"libraryId"\`` and `LibraryName string \`json:"libraryName"\``
+
+6. **Fix compilation** — update the `CreateAudioFile` call in `library.go` `saveAudioFile()` method to include `LibraryID` field. The library ID will be threaded through as a parameter to `Scan`/`scanInternal` (done in Task 2), so for now add the field but use a placeholder `0` value that Task 2 will replace. Actually — since Task 2 immediately follows and both are in the same plan, add `libraryID int64` as a field on the `Library` struct (or better: pass it through the scan methods). For the compilation fix, add `LibraryID: 0` to the CreateAudioFileParams in saveAudioFile — Task 2 will thread the real value.
+
+Verify the generated code compiles: `go build ./backend/...`
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && sqlc generate && go generate ./backend/events/... && go build ./backend/...
+
+ CreateAudioFileParams includes LibraryID field. ScanProgress and ScanMetrics include library identification fields. New scan queue events exist in both Go and TypeScript. Code compiles.
+
+
+
+ Task 2: Create scan queue coordinator and refactor Library for per-library scanning
+
+ backend/library/scan_queue.go
+ backend/library/library.go
+ backend/library/scan_control.go
+ backend/library/config.go
+ backend/library/rescan.go
+
+
+**Create `backend/library/scan_queue.go`** — the scan queue coordinator. This is the core of Phase 11.
+
+Design:
+- The `Library` struct gains scan queue fields (protected by `mu`):
+ - `scanQueue []scanQueueEntry` — FIFO queue of library IDs to scan
+ - `currentScanLibraryID int64` — the library currently being scanned (0 if none)
+ - `currentScanLibraryName string` — for event payloads
+- `scanQueueEntry` struct: `libraryID int64`, `libraryName string`, `libraryPath string`
+
+**Wails-bound methods** (exported, on `*Library`):
+
+1. `ScanLibrary(id int64) error`:
+ - Query `l.db.Queries.GetLibrary(l.ctx, id)` to get library name and path
+ - If library not found, return error
+ - Acquire `l.mu`:
+ - If this library ID is already `currentScanLibraryID` or already in `scanQueue`, return nil (silent dedup per CONTEXT.md)
+ - If no scan is active (`!l.scanActive`), set `currentScanLibraryID = id` and start scanning in a goroutine
+ - If a scan is active, append to `scanQueue` and emit `LibraryScanQueued` event with library name and queue length
+ - Release `l.mu`
+ - Return nil
+
+2. `ScanAllLibraries() error`:
+ - Query `l.db.Queries.GetAllLibraries(l.ctx)` to get all libraries
+ - For each library, call `ScanLibrary(lib.ID)` (reuses dedup logic)
+ - Return nil
+
+3. `CancelCurrentScan()` — cancels only the current library's scan (replaces old `CancelScan`):
+ - Cancel the scan context (existing `l.scanCancel()` call)
+ - The scan completion handler (`drainQueue`) will automatically start the next queued library
+
+4. `CancelAllScans()` — cancels current and clears queue:
+ - Acquire `l.mu`, clear `l.scanQueue`, release `l.mu`
+ - Then cancel the current scan context
+
+5. `GetScanQueueLength() int` — returns length of scan queue (for UI)
+
+**Internal scan orchestration:**
+
+- `startScan(entry scanQueueEntry)` — goroutine entry point:
+ - Calls `l.scanInternal(entry.libraryID, entry.libraryName, entry.libraryPath)`
+ - On completion, calls `l.drainQueue()`
+
+- `drainQueue()` — called after each scan completes:
+ - Acquire `l.mu`
+ - If `scanQueue` is not empty, pop first entry, set as `currentScanLibraryID`, release lock, call `startScan` in new goroutine
+ - If `scanQueue` is empty, set `currentScanLibraryID = 0`, `scanActive = false`, emit `LibraryScanQueueDrained`, release lock
+
+**Refactor `Library.Scan()` → `scanInternal()`:**
+
+- Rename current `Scan()` to `scanInternal(libraryID int64, libraryName string, libraryPath string)` (unexported)
+- Remove the `l.conf.DirectoryPath` dependency — use the `libraryPath` parameter instead
+- Replace `l.db.Queries.GetAllAudioFiles(l.ctx)` with `l.db.Queries.GetAudioFilesByLibrary(l.ctx, libraryID)` in Phase 1 (load existing)
+- Pass `libraryID` through to `saveAudioFile` so `CreateAudioFileParams.LibraryID` is set correctly
+- Update all `ScanProgress` emissions to include `LibraryID`, `LibraryName`, and `QueuedCount` (read queue length under lock)
+- Update `ScanMetrics` to include `LibraryID` and `LibraryName` before emitting `LibraryScanComplete`/`LibraryScanCancelled`
+- The `workerCount` should use `resolveScanWorkerCount(ScanConcurrencyAuto, libraryPath)` — no longer from config (each library path may be on different storage)
+
+**Keep backward-compatible `Scan()` method** — public method that scans using the legacy `l.conf.DirectoryPath` for `handleConfigUpdate`. Mark it deprecated. It should:
+- Look up or create a library for `l.conf.DirectoryPath` using `GetLibraryByPath`
+- Call `ScanLibrary(lib.ID)`
+
+**Update `scan_control.go`:**
+
+- Rename `CancelScan()` to an internal helper `cancelCurrentScan()` (unexported)
+- Keep `PauseScan()` and `ResumeScan()` as-is — they operate on the current scan which is correct
+- `IsScanActive()` unchanged
+- Add `QueuedLibraryNames() []string` — returns names of queued libraries (for UI display)
+
+**Update `config.go`:**
+- The `Config` struct keeps `DirectoryPath` and `ScanConcurrency` for backward compatibility, but `DirectoryPath` is now unused for normal scanning (libraries come from DB). `ScanConcurrency` is still useful as a global default.
+
+**Update `rescan.go`:**
+- `FullRescan()` needs updating — it should accept a library ID. For now, keep it working with `l.conf.DirectoryPath` (it's used from the config page). Phase 12 will add per-library rescan.
+
+**Thread `libraryID` through the scan pipeline:**
+- Add `libraryID int64` field to `scanWork` struct (or pass it via closure)
+- In `saveAudioFile`, use `LibraryID: libraryID` in `CreateAudioFileParams`
+- In the `commitBatch` → `saveAudioFile` call chain, thread the library ID through. Simplest: add `libraryID int64` as a parameter to `commitBatch` and `saveAudioFile` and `updateAudioFileMetadata`.
+
+**Linting notes:**
+- All exported methods need doc comments ending with period (godot)
+- No stuttering (revive) — method names don't repeat "Library"
+- Sentinel errors as package vars (err113)
+- Blank line after early returns (nlreturn)
+- Keep lines under 100 chars (golines)
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && go build ./... && go vet ./backend/library/...
+
+
+- `ScanLibrary(id)` scans a specific library's directory, associating files with that library_id
+- `ScanAllLibraries()` queues all libraries for sequential scanning
+- Scan queue coordinator ensures only one scan runs at a time, with silent dedup
+- Cancel: `CancelCurrentScan()` cancels current and starts next; `CancelAllScans()` cancels current and clears queue
+- Pause freezes current scan AND queue (existing behavior — drainQueue is only called on scan completion, which doesn't happen while paused)
+- All scan events include library name and queue count
+- `go build ./...` passes
+
+
+
+
+
+
+```bash
+# Build passes
+go build ./...
+
+# Vet passes
+go vet ./backend/library/...
+
+# Generated code is up to date
+sqlc generate && go generate ./backend/events/...
+
+# Existing tests still pass (scan_test.go uses the old Scan() path)
+go test ./backend/library/... -count=1 -timeout 60s
+
+# Events synced
+diff <(grep -oP '"[A-Z][a-zA-Z]+"' backend/events/events.go | sort) <(grep -oP '"[A-Z][a-zA-Z]+"' frontend/src/events.ts | sort)
+```
+
+
+
+- ScanLibrary(id) resolves library path from DB and scans only that directory
+- CreateAudioFile includes library_id — new files are associated with their library
+- Only one scan runs at a time — queue coordinates sequential execution
+- Duplicate requests are silently ignored
+- CancelCurrentScan stops current library, next queued starts automatically
+- CancelAllScans stops current and clears queue
+- Pause freezes scan AND queue
+- All scan events include library name and queue count
+- go build ./... passes, go test ./backend/library/... passes
+
+
+
diff --git a/.planning/phases/11-per-library-scan-pipeline/11-02-PLAN.md b/.planning/phases/11-per-library-scan-pipeline/11-02-PLAN.md
new file mode 100644
index 0000000..fddd372
--- /dev/null
+++ b/.planning/phases/11-per-library-scan-pipeline/11-02-PLAN.md
@@ -0,0 +1,245 @@
+---
+phase: 11-per-library-scan-pipeline
+plan: 02
+type: execute
+wave: 2
+depends_on: ["11-01"]
+files_modified:
+ - frontend/src/components/config-page/config-page.ts
+ - frontend/src/components/library-manager/library-manager.ts
+ - frontend/wailsjs/go/library/Library.d.ts
+ - frontend/wailsjs/go/library/Library.js
+autonomous: true
+requirements: [LSCAN-03, LSCAN-04]
+
+must_haves:
+ truths:
+ - "Progress UI shows which library is currently being scanned by name"
+ - "Progress UI shows queue count when libraries are queued"
+ - "Cancel during queued multi-scan shows modal with 'Cancel This Library' and 'Cancel All Scanning' choices"
+ - "Cancelling one library automatically starts scanning the next queued library"
+ - "Scan All Libraries button exists and triggers ScanAllLibraries binding"
+ artifacts:
+ - path: "frontend/src/components/config-page/config-page.ts"
+ provides: "Updated cancel dialog with scope choice, progress with library name"
+ - path: "frontend/src/components/library-manager/library-manager.ts"
+ provides: "Scan All Libraries button, per-library progress display"
+ - path: "frontend/wailsjs/go/library/Library.d.ts"
+ provides: "TypeScript declarations for ScanLibrary, ScanAllLibraries, CancelCurrentScan, CancelAllScans"
+ key_links:
+ - from: "frontend/src/components/config-page/config-page.ts"
+ to: "@go/library/Library"
+ via: "Wails binding calls for CancelCurrentScan, CancelAllScans"
+ pattern: "CancelCurrentScan|CancelAllScans"
+ - from: "frontend/src/components/library-manager/library-manager.ts"
+ to: "@go/library/Library"
+ via: "Wails binding calls for ScanAllLibraries"
+ pattern: "ScanAllLibraries"
+---
+
+
+Update the frontend scan UI to display per-library progress (library name + queue count), add a "Scan All Libraries" button, and implement the cancel scope modal dialog for queued scans.
+
+Purpose: Fulfill LSCAN-03 (progress identifies which library) and LSCAN-04 frontend (cancel/pause work per-library with clear scope).
+Output: Updated config-page with library-aware cancel dialog, library-manager with Scan All button, Wails binding stubs.
+
+
+
+@/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/phases/11-per-library-scan-pipeline/11-CONTEXT.md
+@.planning/phases/11-per-library-scan-pipeline/11-01-SUMMARY.md
+@.planning/phases/09-scan-cancellation-keyboard-shortcuts/09-03-SUMMARY.md
+
+
+
+
+Updated ScanProgress payload (from backend/library/metrics.go after Plan 01):
+```typescript
+interface ScanProgress {
+ phase: 'counting' | 'scanning' | 'orphans' | 'thumbnails';
+ total: number;
+ processed: number;
+ added: number;
+ skipped: number;
+ updated: number;
+ libraryId: number; // NEW — which library is scanning
+ libraryName: string; // NEW — display name
+ queuedCount: number; // NEW — libraries still queued
+}
+```
+
+New Wails-bound methods (from Plan 01):
+```typescript
+// These will need stubs in Library.d.ts and Library.js
+export function ScanLibrary(id: number): Promise;
+export function ScanAllLibraries(): Promise;
+export function CancelCurrentScan(): Promise;
+export function CancelAllScans(): Promise;
+export function GetScanQueueLength(): Promise;
+```
+
+New events (from Plan 01):
+```typescript
+LibraryScanQueued: "LibraryScanQueued",
+LibraryScanQueueDrained: "LibraryScanQueueDrained",
+```
+
+Existing cancel dialog pattern from config-page.ts:
+- Modal overlay with stopPropagation
+- Three button choices
+- handleCancelKeep / handleCancelDiscard / handleCancelDialogDismiss
+
+
+
+
+
+
+ Task 1: Add Wails binding stubs and update progress/cancel UI in config-page
+
+ frontend/wailsjs/go/library/Library.d.ts
+ frontend/wailsjs/go/library/Library.js
+ frontend/src/components/config-page/config-page.ts
+
+
+1. **Add Wails binding stubs** to `frontend/wailsjs/go/library/Library.d.ts`:
+ ```typescript
+ export function ScanLibrary(id: number): Promise;
+ export function ScanAllLibraries(): Promise;
+ export function CancelCurrentScan(): Promise;
+ export function CancelAllScans(): Promise;
+ export function GetScanQueueLength(): Promise;
+ export function QueuedLibraryNames(): Promise;
+ ```
+
+ And corresponding runtime implementations in `Library.js`:
+ ```javascript
+ export function ScanLibrary(id) { return window['go']['library']['Library']['ScanLibrary'](id); }
+ export function ScanAllLibraries() { return window['go']['library']['Library']['ScanAllLibraries'](); }
+ export function CancelCurrentScan() { return window['go']['library']['Library']['CancelCurrentScan'](); }
+ export function CancelAllScans() { return window['go']['library']['Library']['CancelAllScans'](); }
+ export function GetScanQueueLength() { return window['go']['library']['Library']['GetScanQueueLength'](); }
+ export function QueuedLibraryNames() { return window['go']['library']['Library']['QueuedLibraryNames'](); }
+ ```
+
+2. **Update config-page.ts ScanProgress interface** to include the new fields:
+ - Add `libraryId: number`, `libraryName: string`, `queuedCount: number` to the `ScanProgress` interface
+
+3. **Update imports** — replace `CancelScan` import with `CancelCurrentScan, CancelAllScans` from `@go/library/Library`
+
+4. **Update progress display** (`renderScanProgress` method or equivalent):
+ - When `scanProgress.libraryName` is non-empty, show "Scanning: [Library Name]" as the progress label instead of just "Scanning"
+ - When `scanProgress.queuedCount > 0`, add a line below: "[N] libraries queued" in tertiary text color
+ - Format: `Scanning: My Music (245/1200 files)` with `2 libraries queued` below
+
+5. **Update cancel dialog** — replace the current three-option dialog with the per-library-aware version per CONTEXT.md:
+ - Add `@state() private scanQueuedCount = 0;` to track queue state
+ - Update `handleScanProgress` to also save `queuedCount`
+ - **When `queuedCount > 0`** (multi-scan in progress): show modal dialog with TWO buttons:
+ - "Cancel This Library" — calls `CancelCurrentScan()` (stops current, next starts)
+ - "Cancel All Scanning" — calls `CancelAllScans()` (stops everything)
+ - No default — user must pick (per CONTEXT.md: "no default, user must pick")
+ - **When `queuedCount === 0`** (single scan): keep existing cancel behavior but call `CancelCurrentScan()` instead of `CancelScan()`. Can use the existing Keep/Discard/Continue dialog pattern.
+ - Update `handleCancelKeep` → call `CancelCurrentScan()` instead of `CancelScan()`
+ - Update `handleCancelDiscard` → call `CancelCurrentScan()` instead of `CancelScan()`
+
+6. **Handle new events** in `connectedCallback`:
+ - Listen for `LibraryScanQueued` — update `scanQueuedCount` from event payload
+ - Listen for `LibraryScanQueueDrained` — set `scanQueuedCount = 0`, reset scan state
+
+7. **Update scan buttons section** — when not scanning, show "Scan All Libraries" as an additional button alongside Soft Scan and Full Rescan. It calls `ScanAllLibraries()`.
+
+**Styling notes:**
+- Use existing design tokens (`--yj-text-primary`, `--yj-text-tertiary`, `--yj-accent`)
+- Queue count text: `.progress-detail` style (smaller, tertiary color)
+- Library name in progress: bold, primary text color
+- Cancel modal buttons: "Cancel This Library" gets `btn-warning`, "Cancel All Scanning" gets `btn-danger`
+- Keep `.cancel-dialog` CSS class pattern from Phase 9
+
+**TypeScript strictness:**
+- `override` keyword on lifecycle methods
+- `import type` for type-only imports
+- Private event handlers as arrow functions
+
+
+ cd /mnt/vault/dev/golang/yellowjacket/frontend && npx tsc --noEmit
+
+
+- ScanProgress interface includes libraryId, libraryName, queuedCount
+- Progress UI shows "Scanning: [Library Name]" and queue count
+- Cancel dialog shows scope choice when multiple scans queued
+- CancelCurrentScan/CancelAllScans called instead of CancelScan
+- Scan All Libraries button exists in scan actions
+- TypeScript compiles cleanly
+
+
+
+
+ Task 2: Update library-manager component for per-library scan display
+
+ frontend/src/components/library-manager/library-manager.ts
+
+
+1. **Update ScanProgress interface** in library-manager.ts to match the new fields: add `libraryId: number`, `libraryName: string`, `queuedCount: number`.
+
+2. **Update progress rendering** in `renderScanProgress()`:
+ - Show library name: "Scanning: [Library Name]" as the progress label
+ - Show queued count when > 0: "[N] libraries queued" in tertiary text
+
+3. **Update imports** — add `ScanAllLibraries` import from `@go/library/Library`
+
+4. **Add "Scan All Libraries" button** to the scan actions section:
+ - Place it alongside existing "Soft Scan" and "Full Rescan" buttons
+ - Style: `btn-primary` class, disabled when scanning
+ - Handler: `private handleScanAll = async (): Promise => { await ScanAllLibraries(); }`
+ - Label: "Scan All Libraries" (or "Scanning..." when active)
+
+5. **Listen for LibraryScanQueued and LibraryScanQueueDrained events**:
+ - In `connectedCallback`, add event subscriptions
+ - In `disconnectedCallback`, clean up subscriptions
+ - These events update scanning state for the UI
+
+6. **Update handleScanComplete** to handle per-library scan completion:
+ - The `LibraryScanComplete` event now includes `libraryName` in the metrics
+ - If queue is still draining, don't reset scanning state (wait for `LibraryScanQueueDrained`)
+ - Only fully reset `scanning = false` on `LibraryScanQueueDrained` or when `queuedCount === 0` in the complete event
+
+
+ cd /mnt/vault/dev/golang/yellowjacket/frontend && npx tsc --noEmit
+
+
+- Library-manager shows library name in scan progress
+- "Scan All Libraries" button exists and calls ScanAllLibraries
+- Scan state properly tracks queue draining (doesn't reset early)
+- TypeScript compiles cleanly
+
+
+
+
+
+
+```bash
+# TypeScript compiles
+cd frontend && npx tsc --noEmit
+
+# Full project builds (backend + frontend)
+cd .. && go build ./...
+```
+
+
+
+- Progress bar shows "Scanning: [Library Name] (N/M files)" during scan
+- Queue count visible when libraries are queued
+- Cancel modal offers "Cancel This Library" / "Cancel All Scanning" during queued scans
+- "Scan All Libraries" button exists in both config-page and library-manager
+- TypeScript compiles cleanly
+
+
+
diff --git a/.planning/phases/11-per-library-scan-pipeline/11-03-PLAN.md b/.planning/phases/11-per-library-scan-pipeline/11-03-PLAN.md
new file mode 100644
index 0000000..0a06c46
--- /dev/null
+++ b/.planning/phases/11-per-library-scan-pipeline/11-03-PLAN.md
@@ -0,0 +1,182 @@
+---
+phase: 11-per-library-scan-pipeline
+plan: 03
+type: execute
+wave: 2
+depends_on: ["11-01"]
+files_modified:
+ - backend/app.go
+ - backend/library/library.go
+autonomous: true
+requirements: [LSCAN-01, LSCAN-02]
+
+must_haves:
+ truths:
+ - "App auto-scans all libraries on launch using ScanAllLibraries"
+ - "Legacy LibraryConfigChanged event handler is removed or updated for multi-library"
+ - "Library struct no longer requires Config.DirectoryPath to function"
+ artifacts:
+ - path: "backend/app.go"
+ provides: "Updated OnDomReady or OnStartup to trigger ScanAllLibraries on launch"
+ - path: "backend/library/library.go"
+ provides: "Updated NewLibrary constructor — Config no longer required"
+ key_links:
+ - from: "backend/app.go"
+ to: "backend/library/scan_queue.go"
+ via: "ScanAllLibraries call on startup"
+ pattern: "library\\.ScanAllLibraries"
+---
+
+
+Wire the per-library scan pipeline into app startup and clean up legacy single-directory scanning paths.
+
+Purpose: Ensure auto-scan on launch uses `ScanAllLibraries()` (same codepath as the UI button per CONTEXT.md), and remove/update legacy `LibraryConfigChanged` handler that assumed a single directory.
+Output: Updated app.go startup wiring, cleaned-up Library constructor.
+
+
+
+@/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/phases/11-per-library-scan-pipeline/11-CONTEXT.md
+@.planning/phases/11-per-library-scan-pipeline/11-01-SUMMARY.md
+
+
+
+From backend/library/scan_queue.go (created in Plan 01):
+```go
+func (l *Library) ScanLibrary(id int64) error
+func (l *Library) ScanAllLibraries() error
+func (l *Library) CancelCurrentScan()
+func (l *Library) CancelAllScans()
+```
+
+From backend/app.go (current):
+```go
+func (yj *YellowJacketApp) OnStartup(ctx context.Context)
+// Currently: yj.library.SetContext(ctx)
+// Currently: library is created with appConfig.Library (Config with DirectoryPath)
+
+func NewYellowJacketApp(...) {
+ lib, err := library.NewLibrary(
+ yjApp.appContext,
+ yjApp.logger,
+ yjApp.appConfig.Library, // Config with DirectoryPath
+ yjApp.database,
+ )
+}
+```
+
+From backend/library/library.go (current event handler):
+```go
+func (l *Library) registerEventHandlers() {
+ runtime.EventsOn(l.ctx, events.LibraryConfigChanged, func(data ...any) {
+ // Parses DirectoryPath from event data, calls l.handleConfigUpdate
+ })
+}
+```
+
+
+
+
+
+
+ Task 1: Wire auto-scan on startup and clean up legacy single-directory code
+
+ backend/app.go
+ backend/library/library.go
+
+
+1. **Update `NewLibrary` constructor** in `backend/library/library.go`:
+ - Make `*Config` parameter optional/removable. The Library no longer needs a pre-configured DirectoryPath because scan paths come from the database.
+ - Keep the `*Config` parameter for backward compatibility but don't require `DirectoryPath` to be set.
+ - Update validation: if `conf` is nil, create a default config with empty DirectoryPath (already handled).
+
+2. **Update `registerEventHandlers`** in `backend/library/library.go`:
+ - Remove the `LibraryConfigChanged` event handler entirely. This handler assumed a single-directory model where changing the config triggers a scan. In the multi-library model:
+ - Libraries are added/removed through the library CRUD API (Phase 12)
+ - Scanning is triggered explicitly via `ScanLibrary()` or `ScanAllLibraries()`
+ - The `LibraryConfigChanged` event and `handleConfigUpdate` method can be deleted or marked deprecated
+ - Delete `handleConfigUpdate` method
+ - Delete `errLibraryDirNotConfigured` sentinel error (no longer needed)
+
+3. **Update `NewYellowJacketApp` in `backend/app.go`**:
+ - Change the `library.NewLibrary(...)` call. The Config parameter is less important now since DirectoryPath is ignored. Pass `yjApp.appConfig.Library` as before (it still has ScanConcurrency which is useful as a default).
+
+4. **Add auto-scan on startup** in `backend/app.go`:
+ - In `OnDomReady` (or via a goroutine started in `OnStartup` that waits for DOM ready), trigger auto-scan.
+ - Best approach: In `OnDomReady`, after the startup error check, launch a goroutine:
+ ```go
+ go func() {
+ if err := yj.library.ScanAllLibraries(); err != nil {
+ yj.logger.Error("auto-scan failed", "err", err)
+ }
+ }()
+ ```
+ - This uses the same `ScanAllLibraries()` codepath as the UI button (per CONTEXT.md: "Auto-scan on launch should use the same ScanAllLibraries() codepath as the UI button — single implementation").
+ - It runs in a goroutine so it doesn't block the DOM ready callback.
+ - Only run if there are libraries in the DB: check `l.db.Queries.CountLibraries(l.ctx)` first (or let ScanAllLibraries handle the empty case gracefully by returning immediately when GetAllLibraries returns an empty slice).
+
+5. **Clean up legacy `Scan()` method**:
+ - In Plan 01, the old `Scan()` was kept as backward-compatible wrapper. Now review: since we're removing `handleConfigUpdate` which was the only caller of the legacy `Scan()` via `l.handleConfigUpdate → l.Scan()`, we can either:
+ - Keep `Scan()` for tests (it's used in `scan_test.go`)
+ - Update it to call `scanInternal` with the library from `l.conf.DirectoryPath` if set, or return early if not set
+ - Keep `FullRescan()` — it's still called from the config-page UI. It should work with the first/default library. Update it to look up the default library from DB rather than using `l.conf.DirectoryPath`.
+
+6. **Update `FullRescan()`** in `backend/library/rescan.go`:
+ - Instead of using `l.conf.DirectoryPath`, look up the first library from DB: `libs, err := l.db.Queries.GetAllLibraries(l.ctx)` and use `libs[0]`.
+ - If no libraries exist, return an error.
+ - Call `scanInternal(lib.ID, lib.Name, lib.Path)` instead of `l.Scan()`.
+ - Per-library FullRescan will be added in Phase 12 — for now this rescans the first/only library.
+
+**Linting requirements:**
+- Doc comments ending with period
+- Blank line after early returns
+- Lines under 100 chars
+
+
+ cd /mnt/vault/dev/golang/yellowjacket && go build ./... && go vet ./backend/... && go test ./backend/library/... -count=1 -timeout 60s
+
+
+- Auto-scan on startup calls ScanAllLibraries (same codepath as UI button)
+- Legacy LibraryConfigChanged handler removed
+- Legacy handleConfigUpdate removed
+- FullRescan uses library from DB instead of config DirectoryPath
+- go build passes, go vet passes, existing tests pass
+
+
+
+
+
+
+```bash
+# Full build
+go build ./...
+
+# Vet
+go vet ./backend/...
+
+# Tests pass (including scan_test.go)
+go test ./backend/library/... -count=1 -timeout 60s
+
+# No references to removed handler
+grep -rn "LibraryConfigChanged" backend/library/ | grep -v "_test.go"
+# Should return no hits (only events.go constant definition, not handler registration)
+```
+
+
+
+- App auto-scans all libraries on launch via ScanAllLibraries
+- LibraryConfigChanged handler removed from library package
+- handleConfigUpdate removed
+- FullRescan works with DB-sourced library (not config DirectoryPath)
+- All tests pass, build passes
+
+
+