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.
This commit is contained in:
2026-03-05 17:13:21 -05:00
parent ff480129a3
commit 40f303b004
2 changed files with 24 additions and 3 deletions
+1
View File
@@ -88,6 +88,7 @@ func (h *Handler) ScanLibrary(c echo.Context) error {
job := &services.Job{ job := &services.Job{
ID: jobID, ID: jobID,
Type: services.JobTypeScan, Type: services.JobTypeScan,
UserID: userID,
Params: map[string]interface{}{ Params: map[string]interface{}{
"library_id": req.LibraryID, "library_id": req.LibraryID,
"folders": folderPaths, "folders": folderPaths,
+21 -1
View File
@@ -2,6 +2,7 @@ package services
import ( import (
"bookhoard/internal/database" "bookhoard/internal/database"
wsync "bookhoard/internal/sync"
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
@@ -53,6 +54,7 @@ func (w *Worker) Enqueue(job *Job) {
type Job struct { type Job struct {
ID string ID string
Type JobType Type JobType
UserID string
Params map[string]interface{} Params map[string]interface{}
Status JobStatus Status JobStatus
CreatedAt time.Time CreatedAt time.Time
@@ -84,6 +86,7 @@ type JobResult struct {
type Worker struct { type Worker struct {
jobQueue chan *Job jobQueue chan *Job
results map[string]*JobResult results map[string]*JobResult
connManager *wsync.ConnectionManager
mu sync.RWMutex mu sync.RWMutex
wg sync.WaitGroup wg sync.WaitGroup
ctx context.Context ctx context.Context
@@ -91,7 +94,7 @@ type Worker struct {
shuttingDown atomic.Bool shuttingDown atomic.Bool
} }
func NewWorker(numWorkers int) *Worker { func NewWorker(numWorkers int, connManager *wsync.ConnectionManager) *Worker {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
w := &Worker{ w := &Worker{
@@ -106,6 +109,8 @@ func NewWorker(numWorkers int) *Worker {
go w.worker() go w.worker()
} }
w.connManager = connManager
return w return w
} }
@@ -249,6 +254,21 @@ func (w *Worker) processScanJob(job *Job) (interface{}, error) {
result.NewItems = newItems result.NewItems = newItems
result.Errors = errors 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 { if err := scanner.SetFolders(folders); err != nil {