338 lines
12 KiB
Markdown
338 lines
12 KiB
Markdown
---
|
|
phase: 09-scan-cancellation-keyboard-shortcuts
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- backend/events/events.go
|
|
- frontend/src/events.ts
|
|
- backend/library/library.go
|
|
- backend/library/scan_control.go
|
|
- backend/library/metrics.go
|
|
autonomous: true
|
|
requirements:
|
|
- SCAN-01
|
|
- SCAN-02
|
|
- SCAN-03
|
|
|
|
must_haves:
|
|
truths:
|
|
- "CancelScan() cancels the scan context and workers stop at their next checkpoint"
|
|
- "PauseScan() blocks workers via a channel; ResumeScan() unblocks them"
|
|
- "Cancelled scans skip orphan cleanup to avoid deleting unvisited files"
|
|
- "Batch commits use l.ctx (app context), not the cancellable scanCtx, so in-flight transactions complete"
|
|
- "ScanMetrics.Cancelled is true when a scan was cancelled"
|
|
artifacts:
|
|
- path: "backend/library/scan_control.go"
|
|
provides: "CancelScan, PauseScan, ResumeScan, IsScanActive, IsScanPaused methods"
|
|
exports: ["CancelScan", "PauseScan", "ResumeScan", "IsScanActive", "IsScanPaused"]
|
|
- path: "backend/events/events.go"
|
|
provides: "New scan control events"
|
|
contains: "LibraryScanCancelled"
|
|
- path: "backend/library/metrics.go"
|
|
provides: "Cancelled field on ScanMetrics"
|
|
contains: "Cancelled"
|
|
key_links:
|
|
- from: "backend/library/scan_control.go"
|
|
to: "backend/library/library.go"
|
|
via: "scanCancel context.CancelFunc and scanPauseCh channel on Library struct"
|
|
pattern: "l\\.scanCancel|l\\.scanPauseCh"
|
|
- from: "backend/library/library.go"
|
|
to: "backend/events/events.go"
|
|
via: "EventsEmit for scan lifecycle events"
|
|
pattern: "events\\.LibraryScan"
|
|
---
|
|
|
|
<objective>
|
|
Add scan cancellation and pause/resume to the Go backend. Thread a per-scan cancellable context through the existing scan pipeline, add pause/resume via a blocking channel, and expose Wails-bound methods for frontend control.
|
|
|
|
Purpose: Backend foundation for SCAN-01/02/03 — frontend buttons wire to these methods in Plan 03.
|
|
Output: scan_control.go with CancelScan/PauseScan/ResumeScan, modified Scan() method, new events, updated metrics.
|
|
</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/09-scan-cancellation-keyboard-shortcuts/09-RESEARCH.md
|
|
|
|
@backend/library/library.go
|
|
@backend/library/metrics.go
|
|
@backend/events/events.go
|
|
|
|
<interfaces>
|
|
<!-- Library struct (library.go:78-87) — add scan control fields here -->
|
|
type Library struct {
|
|
mu sync.Mutex
|
|
ctx context.Context
|
|
logger *slog.Logger
|
|
conf *Config
|
|
db *database.DB
|
|
rescanHooks RescanHooks
|
|
}
|
|
|
|
<!-- ScanMetrics (metrics.go:11-55) — add Cancelled bool field -->
|
|
type ScanMetrics struct {
|
|
mu sync.Mutex
|
|
// ... existing timing and count fields ...
|
|
Added int64 `json:"added"`
|
|
Updated int64 `json:"updated"`
|
|
Skipped int64 `json:"skipped"`
|
|
Removed int64 `json:"removed"`
|
|
Warnings []ScanWarning `json:"warnings"`
|
|
}
|
|
|
|
<!-- Existing events (events.go:44-48) -->
|
|
const (
|
|
LibraryScanStarted = "LibraryScanStarted"
|
|
LibraryScanProgress = "LibraryScanProgress"
|
|
LibraryScanComplete = "LibraryScanComplete"
|
|
)
|
|
|
|
<!-- Scan() method signature (library.go:175) -->
|
|
func (l *Library) Scan() (*ScanMetrics, error)
|
|
|
|
<!-- Key scan pipeline locations that check l.ctx.Done() -->
|
|
<!-- library.go:297-298: case <-l.ctx.Done(): return l.ctx.Err() (walk, sending to workChan) -->
|
|
<!-- library.go:324-325: case <-l.ctx.Done(): return l.ctx.Err() (walk, new file) -->
|
|
<!-- library.go:496-497: case <-l.ctx.Done(): return l.ctx.Err() (worker, sending to resultChan) -->
|
|
|
|
<!-- commitBatch called at library.go:433 — uses l.ctx implicitly for DB ops -->
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add scan control events and metrics fields</name>
|
|
<files>backend/events/events.go, frontend/src/events.ts, backend/library/metrics.go</files>
|
|
<action>
|
|
1. In `backend/events/events.go`, add a new const block for scan control events:
|
|
```go
|
|
// Scan control events.
|
|
const (
|
|
LibraryScanCancelled = "LibraryScanCancelled"
|
|
LibraryScanPaused = "LibraryScanPaused"
|
|
LibraryScanResumed = "LibraryScanResumed"
|
|
)
|
|
```
|
|
Place it after the existing Library events block (line 48).
|
|
|
|
2. Run `go generate ./backend/events/...` to regenerate `frontend/src/events.ts`.
|
|
|
|
3. In `backend/library/metrics.go`, add a `Cancelled` field to `ScanMetrics`:
|
|
```go
|
|
Cancelled bool `json:"cancelled"`
|
|
```
|
|
Place it after the `Removed int64` field (line 51), before the `Warnings` field.
|
|
</action>
|
|
<verify>
|
|
<automated>cd backend && go build ./... && go generate ./events/... && grep -q "LibraryScanCancelled" events/events.go && grep -q "LibraryScanCancelled" ../frontend/src/events.ts && grep -q "Cancelled" library/metrics.go</automated>
|
|
</verify>
|
|
<done>Three new scan control events exist in events.go and are synced to frontend/src/events.ts. ScanMetrics has a Cancelled bool field.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add scan control fields to Library struct and create scan_control.go</name>
|
|
<files>backend/library/library.go, backend/library/scan_control.go</files>
|
|
<action>
|
|
1. In `backend/library/library.go`, add scan control fields to the `Library` struct (after `rescanHooks` at line 86):
|
|
```go
|
|
// Scan control fields — protected by mu.
|
|
scanActive bool
|
|
scanCancel context.CancelFunc
|
|
scanPaused bool
|
|
scanPauseCh chan struct{}
|
|
```
|
|
|
|
2. Create `backend/library/scan_control.go` with these Wails-bound methods:
|
|
|
|
```go
|
|
package library
|
|
|
|
import (
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
"yellowjacket/backend/events"
|
|
)
|
|
|
|
// CancelScan cancels an in-progress scan. Returns immediately;
|
|
// scan goroutines stop at their next checkpoint.
|
|
func (l *Library) CancelScan() {
|
|
l.mu.Lock()
|
|
cancel := l.scanCancel
|
|
l.mu.Unlock()
|
|
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
// PauseScan pauses an in-progress scan. Workers block at their
|
|
// next pause checkpoint until ResumeScan is called.
|
|
func (l *Library) PauseScan() {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
if !l.scanActive || l.scanPaused {
|
|
return
|
|
}
|
|
|
|
l.scanPaused = true
|
|
l.scanPauseCh = make(chan struct{})
|
|
|
|
runtime.EventsEmit(l.ctx, events.LibraryScanPaused)
|
|
}
|
|
|
|
// ResumeScan unblocks a paused scan.
|
|
func (l *Library) ResumeScan() {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
if !l.scanPaused {
|
|
return
|
|
}
|
|
|
|
l.scanPaused = false
|
|
close(l.scanPauseCh) // unblocks all waiting workers
|
|
|
|
runtime.EventsEmit(l.ctx, events.LibraryScanResumed)
|
|
}
|
|
|
|
// IsScanActive returns whether a scan is currently running.
|
|
func (l *Library) IsScanActive() bool {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.scanActive
|
|
}
|
|
|
|
// IsScanPaused returns whether the scan is currently paused.
|
|
func (l *Library) IsScanPaused() bool {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.scanPaused
|
|
}
|
|
|
|
// waitIfPaused blocks the calling goroutine if the scan is paused.
|
|
// Returns ctx.Err() if the context is cancelled while waiting.
|
|
func (l *Library) waitIfPaused(ctx context.Context) error {
|
|
l.mu.Lock()
|
|
ch := l.scanPauseCh
|
|
paused := l.scanPaused
|
|
l.mu.Unlock()
|
|
|
|
if !paused || ch == nil {
|
|
return nil
|
|
}
|
|
|
|
select {
|
|
case <-ch: // closed = unpaused
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
```
|
|
|
|
Note: `waitIfPaused` takes a `context.Context` parameter (the scan-specific context), not `l.ctx`. Add `"context"` to the import block.
|
|
|
|
3. Modify `Scan()` in `backend/library/library.go`:
|
|
|
|
a. At the top of Scan() (after `metrics := newScanMetrics()`, line 176), create a cancellable scan context:
|
|
```go
|
|
scanCtx, scanCancel := context.WithCancel(l.ctx)
|
|
defer scanCancel()
|
|
|
|
l.mu.Lock()
|
|
l.scanCancel = scanCancel
|
|
l.scanActive = true
|
|
l.scanPaused = false
|
|
l.scanPauseCh = nil
|
|
l.mu.Unlock()
|
|
|
|
defer func() {
|
|
l.mu.Lock()
|
|
l.scanCancel = nil
|
|
l.scanActive = false
|
|
// If still paused, unpause so no dangling channel
|
|
if l.scanPaused {
|
|
l.scanPaused = false
|
|
if l.scanPauseCh != nil {
|
|
close(l.scanPauseCh)
|
|
}
|
|
}
|
|
l.scanPauseCh = nil
|
|
l.mu.Unlock()
|
|
}()
|
|
```
|
|
|
|
b. Replace ALL occurrences of `<-l.ctx.Done()` inside Scan() with `<-scanCtx.Done()`, and `l.ctx.Err()` with `scanCtx.Err()` (the walk goroutine send-to-workChan selects and the walk error return, and the worker pool send-to-resultChan select). There are 3 occurrences: line ~297, ~324, ~496.
|
|
|
|
c. In the worker pool loop (Phase 3, around line 474), add a pause checkpoint before processing each file. Add at the start of the `g.Go(func() error {` closure body:
|
|
```go
|
|
if err := l.waitIfPaused(scanCtx); err != nil {
|
|
return err
|
|
}
|
|
```
|
|
|
|
d. **CRITICAL — Batch commits use l.ctx, NOT scanCtx:** The `commitBatch` method and all DB operations within it should continue to use `l.ctx` (the app context), NOT the scan-specific `scanCtx`. This is already the case since `commitBatch` accesses `l.ctx` internally. DO NOT change `commitBatch` to use `scanCtx`. This ensures in-flight transactions always complete even when the scan is cancelled.
|
|
|
|
e. **CRITICAL — Skip orphan cleanup on cancelled scan:** Before the orphan cleanup phase (Phase 5, around line 549), add a check:
|
|
```go
|
|
// Skip orphan cleanup if the scan was cancelled — existingPaths
|
|
// still contains unvisited files that would be incorrectly deleted.
|
|
cancelled := scanCtx.Err() != nil
|
|
if cancelled {
|
|
metrics.Cancelled = true
|
|
l.logger.Info("scan cancelled, skipping orphan cleanup")
|
|
} else {
|
|
// ... existing orphan cleanup code ...
|
|
}
|
|
```
|
|
Wrap the existing orphan cleanup code (existingPaths.Range through metrics.OrphanCleanup = ...) inside the `else` block.
|
|
|
|
f. Also skip the "Phase 6: post-scan variant generation" if cancelled (wrap in same `if !cancelled` check or separate check).
|
|
|
|
g. When the scan was cancelled, emit `LibraryScanCancelled` instead of (or in addition to) `LibraryScanComplete`. Update the finalize section:
|
|
```go
|
|
if cancelled {
|
|
runtime.EventsEmit(l.ctx, events.LibraryScanCancelled, metrics)
|
|
} else {
|
|
runtime.EventsEmit(l.ctx, events.LibraryScanComplete, metrics)
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>cd backend && go build ./... && go vet ./library/...</automated>
|
|
</verify>
|
|
<done>Library struct has scan control fields. scan_control.go provides CancelScan/PauseScan/ResumeScan/IsScanActive/IsScanPaused. Scan() uses per-scan context, workers check for pause, orphan cleanup is skipped on cancel, and appropriate events are emitted.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
```bash
|
|
cd backend && go build ./... && go vet ./library/... && go vet ./events/...
|
|
```
|
|
All backend code compiles. No vet errors. New scan control methods are exported and Wails-bindable.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- `go build ./...` passes with no errors
|
|
- `CancelScan`, `PauseScan`, `ResumeScan`, `IsScanActive`, `IsScanPaused` are exported methods on `*Library`
|
|
- `waitIfPaused` is an unexported helper that blocks on pause channel
|
|
- Scan() creates a per-scan context and uses it for worker cancellation
|
|
- Orphan cleanup and variant generation are skipped when scan is cancelled
|
|
- `LibraryScanCancelled`, `LibraryScanPaused`, `LibraryScanResumed` events exist and are synced to TypeScript
|
|
- `ScanMetrics.Cancelled` bool field exists
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/09-scan-cancellation-keyboard-shortcuts/09-01-SUMMARY.md`
|
|
</output>
|