From 40f303b0043396260f36cc380f12efc03244622f Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 5 Mar 2026 17:13:21 -0500 Subject: [PATCH] feat: add user-scoped WebSocket broadcasting for scan progress - Add UserID field to Job struct for tracking job ownership - Broadcast scan progress updates to user's WebSocket connections - Send real-time updates during scanning (progress, files scanned, new items, errors) This allows the frontend to display live scan progress without HTTP polling. Scanner now associates scan jobs with requesting user for targeted updates. --- internal/handlers/scanner.go | 5 +++-- internal/services/worker.go | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/internal/handlers/scanner.go b/internal/handlers/scanner.go index 0efa9ba..d2c1430 100644 --- a/internal/handlers/scanner.go +++ b/internal/handlers/scanner.go @@ -86,8 +86,9 @@ func (h *Handler) ScanLibrary(c echo.Context) error { jobID := uuid.New().String() job := &services.Job{ - ID: jobID, - Type: services.JobTypeScan, + ID: jobID, + Type: services.JobTypeScan, + UserID: userID, Params: map[string]interface{}{ "library_id": req.LibraryID, "folders": folderPaths, diff --git a/internal/services/worker.go b/internal/services/worker.go index 9ebf5f7..5f0e09e 100644 --- a/internal/services/worker.go +++ b/internal/services/worker.go @@ -2,6 +2,7 @@ package services import ( "bookhoard/internal/database" + wsync "bookhoard/internal/sync" "context" "fmt" "net/http" @@ -53,6 +54,7 @@ func (w *Worker) Enqueue(job *Job) { type Job struct { ID string Type JobType + UserID string Params map[string]interface{} Status JobStatus CreatedAt time.Time @@ -84,6 +86,7 @@ type JobResult struct { type Worker struct { jobQueue chan *Job results map[string]*JobResult + connManager *wsync.ConnectionManager mu sync.RWMutex wg sync.WaitGroup ctx context.Context @@ -91,7 +94,7 @@ type Worker struct { shuttingDown atomic.Bool } -func NewWorker(numWorkers int) *Worker { +func NewWorker(numWorkers int, connManager *wsync.ConnectionManager) *Worker { ctx, cancel := context.WithCancel(context.Background()) w := &Worker{ @@ -106,6 +109,8 @@ func NewWorker(numWorkers int) *Worker { go w.worker() } + w.connManager = connManager + return w } @@ -249,6 +254,21 @@ func (w *Worker) processScanJob(job *Job) (interface{}, error) { result.NewItems = newItems result.Errors = errors } + // Broadcast via WebSocket to user + if w.connManager != nil && job.UserID != "" { + msg := wsync.BroadcastMessage{ + Type: wsync.MessageTypeScanProgress, + Data: map[string]interface{}{ + "job_id": job.ID, + "progress": progress, + "files_scanned": filesScanned, + "new_items": newItems, + "errors": errors, + }, + } + + w.connManager.BroadcastToUser(job.UserID, msg) + } } if err := scanner.SetFolders(folders); err != nil {