docs(11): create phase plan for per-library scan pipeline
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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`.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
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)
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add library_id to CreateAudioFile + update events and progress types</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<action>
|
||||
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/...`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /mnt/vault/dev/golang/yellowjacket && sqlc generate && go generate ./backend/events/... && go build ./backend/...</automated>
|
||||
</verify>
|
||||
<done>CreateAudioFileParams includes LibraryID field. ScanProgress and ScanMetrics include library identification fields. New scan queue events exist in both Go and TypeScript. Code compiles.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create scan queue coordinator and refactor Library for per-library scanning</name>
|
||||
<files>
|
||||
backend/library/scan_queue.go
|
||||
backend/library/library.go
|
||||
backend/library/scan_control.go
|
||||
backend/library/config.go
|
||||
backend/library/rescan.go
|
||||
</files>
|
||||
<action>
|
||||
**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)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /mnt/vault/dev/golang/yellowjacket && go build ./... && go vet ./backend/library/...</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- `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
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```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)
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-per-library-scan-pipeline/11-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from Plan 01 -->
|
||||
|
||||
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<void>;
|
||||
export function ScanAllLibraries(): Promise<void>;
|
||||
export function CancelCurrentScan(): Promise<void>;
|
||||
export function CancelAllScans(): Promise<void>;
|
||||
export function GetScanQueueLength(): Promise<number>;
|
||||
```
|
||||
|
||||
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
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add Wails binding stubs and update progress/cancel UI in config-page</name>
|
||||
<files>
|
||||
frontend/wailsjs/go/library/Library.d.ts
|
||||
frontend/wailsjs/go/library/Library.js
|
||||
frontend/src/components/config-page/config-page.ts
|
||||
</files>
|
||||
<action>
|
||||
1. **Add Wails binding stubs** to `frontend/wailsjs/go/library/Library.d.ts`:
|
||||
```typescript
|
||||
export function ScanLibrary(id: number): Promise<void>;
|
||||
export function ScanAllLibraries(): Promise<void>;
|
||||
export function CancelCurrentScan(): Promise<void>;
|
||||
export function CancelAllScans(): Promise<void>;
|
||||
export function GetScanQueueLength(): Promise<number>;
|
||||
export function QueuedLibraryNames(): Promise<string[]>;
|
||||
```
|
||||
|
||||
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
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /mnt/vault/dev/golang/yellowjacket/frontend && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- 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
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Update library-manager component for per-library scan display</name>
|
||||
<files>
|
||||
frontend/src/components/library-manager/library-manager.ts
|
||||
</files>
|
||||
<action>
|
||||
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<void> => { 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
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /mnt/vault/dev/golang/yellowjacket/frontend && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- 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
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
# TypeScript compiles
|
||||
cd frontend && npx tsc --noEmit
|
||||
|
||||
# Full project builds (backend + frontend)
|
||||
cd .. && go build ./...
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-per-library-scan-pipeline/11-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 -->
|
||||
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
|
||||
})
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Wire auto-scan on startup and clean up legacy single-directory code</name>
|
||||
<files>
|
||||
backend/app.go
|
||||
backend/library/library.go
|
||||
</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /mnt/vault/dev/golang/yellowjacket && go build ./... && go vet ./backend/... && go test ./backend/library/... -count=1 -timeout 60s</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- 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
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```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)
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-per-library-scan-pipeline/11-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user