feat(collections): add real-time updates via WebSocket (Limitation #4)

Implement real-time collection updates when books are added/removed:

Backend Changes:
- Added connManager to CollectionHandler struct
- Updated constructor to accept ConnectionManager
- Updated all NewCollectionHandler() calls in ebook.go and main.go
- Added WebSocket broadcasts in AddBooks() handler
- Added WebSocket broadcasts in BulkRemoveBooks() handler
- Broadcasts collection_updated events with:
  - collection_id: Which collection changed
  - action: books_added or books_removed
  - book_ids: Array of affected book IDs
  - count: Number of books changed

Frontend Changes:
- Added WebSocket connection in collections UI
- connectWebSocket() establishes connection to /ws/sync
- Listens for collection_updated events
- Shows toast notification on collection change
- Auto-reloads page after 1 second to show updated book list
- Auto-reconnect on disconnect (5s delay)
- Error handling for WebSocket failures

WebSocket Event Format:
{
  "type": "collection_updated",
  "timestamp": "2026-02-01T12:00:00Z",
  "data": {
    "collection_id": "uuid",
    "action": "books_added",
    "book_ids": ["uuid1", "uuid2"],
    "count": 2
  }
}

User Experience:
- When another user adds books to a collection, all connected clients see:
  1. Toast notification: "Collection updated: books_added (2 books)"
  2. Page auto-refreshes after 1 second
  3. Updated book list displays
- Same for book removal
- Works across multiple browser tabs/devices
- No manual refresh needed

Technical Notes:
- Broadcasts to ALL connected WebSocket clients
- Client-side filtering by collection_id
- Existing progress/conflict broadcasts continue to work
- Connection manager handles broadcast distribution

Resolves Limitation #4: Real-time Collection Updates
This commit is contained in:
2026-02-01 00:54:41 -05:00
parent 4bc7a842b5
commit 40c732481a
5 changed files with 78 additions and 6 deletions
+40 -2
View File
@@ -3,11 +3,13 @@ package handlers
import (
"bookmann/internal/database"
"bookmann/internal/services"
wsync "bookmann/internal/sync"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
@@ -17,12 +19,14 @@ import (
type CollectionHandler struct {
db *database.Queries
collectionService *services.CollectionService
connManager *wsync.ConnectionManager
}
func NewCollectionHandler(db *database.Queries) *CollectionHandler {
func NewCollectionHandler(db *database.Queries, connManager *wsync.ConnectionManager) *CollectionHandler {
return &CollectionHandler{
db: db,
collectionService: services.NewCollectionService(db),
connManager: connManager,
}
}
@@ -276,12 +280,31 @@ func (h *CollectionHandler) AddBooks(c echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
addedCount := 0
var addedBookIDs []string
for _, bookIDStr := range req.BookIDs {
bookID, err := uuid.Parse(bookIDStr)
if err != nil {
continue
}
h.collectionService.AddBookToCollection(c.Request().Context(), collectionID, bookID, userUUID)
err = h.collectionService.AddBookToCollection(c.Request().Context(), collectionID, bookID, userUUID)
if err == nil {
addedCount++
addedBookIDs = append(addedBookIDs, bookID.String())
}
}
if addedCount > 0 && h.connManager != nil {
h.connManager.Broadcast(wsync.BroadcastMessage{
Type: "collection_updated",
Timestamp: time.Now().Format(time.RFC3339),
Data: map[string]interface{}{
"collection_id": collectionID.String(),
"action": "books_added",
"book_ids": addedBookIDs,
"count": addedCount,
},
})
}
return c.NoContent(http.StatusNoContent)
@@ -325,6 +348,7 @@ func (h *CollectionHandler) BulkRemoveBooks(c echo.Context) error {
}
removedCount := 0
var removedBookIDs []string
for _, bookIDStr := range req.BookIDs {
bookID, err := uuid.Parse(bookIDStr)
if err != nil {
@@ -334,9 +358,23 @@ func (h *CollectionHandler) BulkRemoveBooks(c echo.Context) error {
err = h.collectionService.RemoveBookFromCollection(c.Request().Context(), collectionID, bookID)
if err == nil {
removedCount++
removedBookIDs = append(removedBookIDs, bookID.String())
}
}
if removedCount > 0 && h.connManager != nil {
h.connManager.Broadcast(wsync.BroadcastMessage{
Type: "collection_updated",
Timestamp: time.Now().Format(time.RFC3339),
Data: map[string]interface{}{
"collection_id": collectionID.String(),
"action": "books_removed",
"book_ids": removedBookIDs,
"count": removedCount,
},
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"removed": removedCount,
"total": len(req.BookIDs),