12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-scan-cancellation-keyboard-shortcuts | 01 | execute | 1 |
|
true |
|
|
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.
<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>
@.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
type Library struct { mu sync.Mutex ctx context.Context logger *slog.Logger conf *Config db *database.DB rescanHooks RescanHooks }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"
}
const ( LibraryScanStarted = "LibraryScanStarted" LibraryScanProgress = "LibraryScanProgress" LibraryScanComplete = "LibraryScanComplete" )
func (l *Library) Scan() (*ScanMetrics, error)
Task 1: Add scan control events and metrics fields backend/events/events.go, frontend/src/events.ts, backend/library/metrics.go 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).-
Run
go generate ./backend/events/...to regeneratefrontend/src/events.ts. -
In
backend/library/metrics.go, add aCancelledfield toScanMetrics:Cancelled bool `json:"cancelled"`Place it after the
Removed int64field (line 51), before theWarningsfield. 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 Three new scan control events exist in events.go and are synced to frontend/src/events.ts. ScanMetrics has a Cancelled bool field.
-
Create
backend/library/scan_control.gowith these Wails-bound methods: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:
waitIfPausedtakes acontext.Contextparameter (the scan-specific context), notl.ctx. Add"context"to the import block. -
Modify
Scan()inbackend/library/library.go:a. At the top of Scan() (after
metrics := newScanMetrics(), line 176), create a cancellable scan context: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(), andl.ctx.Err()withscanCtx.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:if err := l.waitIfPaused(scanCtx); err != nil { return err }d. CRITICAL — Batch commits use l.ctx, NOT scanCtx: The
commitBatchmethod and all DB operations within it should continue to usel.ctx(the app context), NOT the scan-specificscanCtx. This is already the case sincecommitBatchaccessesl.ctxinternally. DO NOT changecommitBatchto usescanCtx. 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:
// 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
elseblock.f. Also skip the "Phase 6: post-scan variant generation" if cancelled (wrap in same
if !cancelledcheck or separate check).g. When the scan was cancelled, emit
LibraryScanCancelledinstead of (or in addition to)LibraryScanComplete. Update the finalize section:if cancelled { runtime.EventsEmit(l.ctx, events.LibraryScanCancelled, metrics) } else { runtime.EventsEmit(l.ctx, events.LibraryScanComplete, metrics) }
<success_criteria>
go build ./...passes with no errorsCancelScan,PauseScan,ResumeScan,IsScanActive,IsScanPausedare exported methods on*LibrarywaitIfPausedis 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,LibraryScanResumedevents exist and are synced to TypeScriptScanMetrics.Cancelledbool field exists </success_criteria>