From f34914166d2f2b44c44c7c2c9aa1ab37e9a2b43d Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 19 Feb 2026 20:48:32 -0500 Subject: [PATCH] docs: Add Phase 4.6 - CreateCollection manual books support Add comprehensive documentation for Phase 4.6 which enables the CreateCollection endpoint to support manual book selection alongside auto-assign rules. This is required for the Custom Section Builder. Key additions: - Phase 4.6: Update CreateCollection Endpoint (30-45 min) - Add ManualBookIDs field to CreateCollectionRequest struct - Implement graceful handling of invalid book IDs - Add validation (max 50 book IDs) to prevent DoS - Reuse existing AddBookToCollection service method - Maintain backward compatibility (field is optional) - Updated Phase 12.5: Collections Bruno tests - create-collection-with-manual-books.bru - create-collection-too-many-books.bru (validation test) - create-collection-invalid-book-id.bru - create-collection-rules-only.bru - create-collection-unauthorized.bru - Added section 13.3: Collections API documentation - manual_book_ids field documentation - Validation limits (max 50 items) - Example combining auto-assign + manual books - Error handling explanation Design decisions: - Graceful degradation: Collection created even if some books fail - Reuse existing infrastructure: No new service methods needed - Backward compatible: Optional field doesn't break existing clients - UI constraint: 50 book limit prevents abuse while allowing flexibility --- CAROUSEL_DASHBOARD_PLAN.md | 612 +++++++++++++++++++++++++++++++------ 1 file changed, 520 insertions(+), 92 deletions(-) diff --git a/CAROUSEL_DASHBOARD_PLAN.md b/CAROUSEL_DASHBOARD_PLAN.md index 5b23849..4988dd0 100644 --- a/CAROUSEL_DASHBOARD_PLAN.md +++ b/CAROUSEL_DASHBOARD_PLAN.md @@ -1069,6 +1069,138 @@ post: --- +### **Phase 4.6: Update CreateCollection Endpoint** (30-45 min) + +**REQUIRED for Custom Section Builder**: The `CreateCollection` endpoint must support adding manual books when creating a collection. + +**Why this is needed:** +- Custom Section Builder allows users to select books manually AND use filter rules +- Both features can be combined (rules + manual selection) +- Single API call is cleaner than separate create + add operations + +**File: `internal/handlers/collections.go`** (MODIFY existing) + +**Step 1: Add `ManualBookIDs` field to `CreateCollectionRequest`** + +After line 40, add the new field: + +```go +type CreateCollectionRequest struct { + Name string `json:"name" validate:"required"` + Description string `json:"description"` + Color string `json:"color"` + Icon string `json:"icon"` + AutoAssignRules []services.Rule `json:"auto_assign_rules"` + ViewSettings map[string]interface{} `json:"view_settings"` + ManualBookIDs []string `json:"manual_book_ids" validate:"max=50"` // NEW +} +``` + +**Step 2: Update `CreateCollection` handler** + +Modify the `CreateCollection` function (lines 73-112) to handle manual books: + +```go +func (h *CollectionHandler) CreateCollection(c echo.Context) error { + user := c.Get("user").(database.Users) + userUUID := uuid.UUID(user.ID.Bytes) + + var req CreateCollectionRequest + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) + } + if err := c.Validate(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + collection, err := h.collectionService.CreateCollection( + c.Request().Context(), + userUUID, + req.Name, + req.Description, + req.Color, + req.Icon, + req.AutoAssignRules, + req.ViewSettings, + ) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + // NEW: Add manual books if provided + if len(req.ManualBookIDs) > 0 { + collectionUUID := uuid.UUID(collection.ID.Bytes) + addedCount := 0 + + for _, bookIDStr := range req.ManualBookIDs { + bookID, err := uuid.Parse(bookIDStr) + if err != nil { + // Skip invalid book IDs, log error + c.Logger().Errorf("Invalid book ID %s: %v", bookIDStr, err) + continue + } + + err = h.collectionService.AddBookToCollection(c.Request().Context(), collectionUUID, bookID, userUUID) + if err != nil { + // Log error but continue adding other books + c.Logger().Errorf("Failed to add book %s to collection: %v", bookIDStr, err) + } else { + addedCount++ + } + } + + c.Logger().Infof("Added %d/%d manual books to collection %s", addedCount, len(req.ManualBookIDs), collection.Name) + } + + bookCount := int32(0) + return c.JSON(http.StatusCreated, map[string]interface{}{ + "id": uuid.UUID(collection.ID.Bytes).String(), + "user_id": uuid.UUID(collection.UserID.Bytes).String(), + "name": collection.Name, + "description": textToString(collection.Description), + "color": textToString(collection.Color), + "icon": textToString(collection.Icon), + "auto_assign_rules": collection.AutoAssignRules, + "view_settings": collection.ViewSettings, + "book_count": bookCount, + "created_at": collection.CreatedAt.Time.String(), + }) +} +``` + +**Key Implementation Details:** +- ✅ Reuses existing `AddBookToCollection` service method +- ✅ Validates request (max 50 book IDs) +- ✅ Returns 400 if more than 50 book IDs provided +- ✅ Gracefully handles invalid book IDs (skips them, logs error) +- ✅ Continues adding remaining books if one fails +- ✅ Backward compatible (field is optional) +- ✅ No database schema changes needed + +**Validation Rule:** +```go +// Add to validator in main.go (around line 130) +v.RegisterValidation("max", func(fl validator.FieldLevel) bool { + field := fl.Field() + if field.Kind() != reflect.Slice { + return true + } + return field.Len() <= 50 +}) +``` + +**Testing:** +```bash +# Verify compilation +go build ./internal/handlers/... + +# Manual test with Bruno +cd bruno/collections +bru run --env local create-collection-with-manual-books.bru +``` + +--- + ### **Phase 5: Bruno API Tests** (1 hour) **File: `bruno/dashboard/**`** (update existing tests) @@ -1183,6 +1315,7 @@ type Config struct { LoginTracker *ratelimit.LoginAttemptTracker ScannerHandler *handlers.Handler DashboardService *services.DashboardService // NEW: For dashboard data fetching + DashboardHandler *handlers.DashboardHandler // NEW: For dashboard API endpoints } ``` @@ -2750,111 +2883,135 @@ No additional work needed - this section references the preview endpoint added e --- ### **Phase 10.6: Final Integration & Testing** (1 hour) - user := c.Get("user").(database.Users) - userUUID := uuid.UUID(user.ID.Bytes) - var req struct { - LibraryID string `json:"library_id"` - Rules []Rule `json:"rules"` - ManualBookIDs []string `json:"manual_book_ids"` - Limit int `json:"limit"` - } +**CRITICAL**: Before proceeding to Phase 11 (Unit Tests), verify all components integrate correctly. - if err := c.Bind(&req); err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"}) - } +#### Verification Checklist - libUUID, err := uuid.Parse(req.LibraryID) - if err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"}) - } +**Build Verification:** +- [ ] TypeScript modules compile: `npm run build:ts` + - Verify: `web/static/dashboard.js` exists + - Verify: `web/static/custom-section-builder.js` exists + - Check for no compilation errors +- [ ] Templates generate successfully: `templ generate --path templates` + - Verify: `templates/dashboard_templ.go` exists + - Verify: `templates/custom_section_templ.go` exists +- [ ] Go build succeeds: `go build ./cmd/server` + - Verify: No compilation errors + - Check all imports resolve correctly - if req.Limit <= 0 || req.Limit > 100 { - req.Limit = 20 - } +**Bruno API Tests:** +- [ ] Dashboard endpoints pass: `cd bruno/dashboard && bru run --env local` + - get-sections-success.bru + - get-sections-missing-library-id.bru + - get-sections-unauthorized.bru + - put-preferences-success.bru + - restore-system-collection-success.bru + - restore-system-collection-invalid-name.bru +- [ ] Collections preview tests pass: + - preview-collection-success.bru + - preview-collection-manual-selection.bru + - preview-collection-combined.bru + - preview-collection-invalid-library.bru - // Get all library items - allItems, err := h.db.GetLibraryItems(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true}) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load library items"}) - } +**Manual Integration Testing:** +- [ ] Dashboard loads successfully + - Navigate to `/dashboard?library_id=` + - Verify 4 system collections appear + - Verify collections show books correctly +- [ ] Library switching works + - Select different library from dropdown + - Verify page updates without full reload + - Verify loading spinner appears/disappears +- [ ] Dashboard settings modal functions + - Open settings modal + - Toggle collection visibility + - Drag to reorder collections + - Save preferences + - Verify changes persist on page reload +- [ ] System collection restore works + - Customize a system collection (hide it) + - Click "Restore" button + - Confirm restoration + - Verify collection reappears with defaults +- [ ] Custom section builder works end-to-end + - Navigate to `/custom-section` + - Add filter rules + - Search and select books manually + - Click "Refresh Preview" + - Verify preview shows matching books + - Save custom section + - Verify section appears on dashboard - // Evaluate rules for each item - var matchedItems []database.MediaItems - for _, item := range allItems { - evaluations := h.collectionService.EvaluateRules(item, req.Rules) - for _, eval := range evaluations { - if eval.Matches { - matchedItems = append(matchedItems, item) - break - } - } - } +**Type Safety Verification:** +- [ ] API responses match TypeScript types + - Check `is_system` is boolean (not string) + - Check `media_item_id` field exists (not `id`) + - Verify field names match (`hidden_collections`, `collection_order`) +- [ ] No TypeScript type errors + - Check browser console for type errors + - Verify all API calls use correct field names - // Add manually selected books - for _, bookID := range req.ManualBookIDs { - bookUUID, err := uuid.Parse(bookID) - if err != nil { - continue - } +**Database Verification:** +- [ ] System collections exist + ```sql + SELECT name, query_type, priority, is_system_collection + FROM collections + WHERE user_id IS NULL; + ``` + Should return 4 rows +- [ ] User preferences table exists + ```sql + \d user_dashboard_preferences + ``` + Verify all columns present - for _, item := range allItems { - itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) - if itemUUID == bookUUID { - // Check if already in matched items - alreadyAdded := false - for _, added := range matchedItems { - addedUUID, _ := uuid.FromBytes(added.ID.Bytes[0:16]) - if addedUUID == bookUUID { - alreadyAdded = true - break - } - } - if !alreadyAdded { - matchedItems = append(matchedItems, item) - } - break - } - } - } +**Performance Smoke Test:** +- [ ] Dashboard loads within 2 seconds + - Test with library containing 100+ items + - Verify carousel scrolling is smooth + - Check no memory leaks in browser console - // Limit results - if len(matchedItems) > req.Limit { - matchedItems = matchedItems[:req.Limit] - } +**Error Handling Verification:** +- [ ] Invalid library_id shows error +- [ ] Unauthorized requests return 401 +- [ ] Network errors show toast notifications +- [ ] Empty collections display "No items" message - // Convert to BookInfo for response - var bookCards []BookInfo - for _, item := range matchedItems { - itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) - bookCards = append(bookCards, BookInfo{ - MediaItemID: itemUUID.String(), - Title: item.Title, - Author: textToString(item.Author), - CoverImagePath: textToString(item.CoverImagePath), - }) - } +#### Troubleshooting Common Issues - return c.JSON(http.StatusOK, map[string]interface{}{"items": bookCards}) -} -``` +**Issue: Collections not appearing** +- Check `show_on_dashboard = true` in database +- Verify user hasn't hidden collection in preferences +- Check browser console for JavaScript errors -Add route to `internal/router/dashboard.go`: +**Issue: TypeScript compilation fails** +- Verify all type definitions in `web/src/types/api.d.ts` +- Check import statements use correct paths +- Ensure no missing dependencies in `package.json` -```go -// In registerDashboardRoutes function: -collections.POST("/preview", cfg.CollectionHandler.PreviewCollection) -``` +**Issue: Templates don't generate** +- Verify template syntax is correct +- Check for unclosed tags +- Run `go install github.com/a-h/templ/cmd/templ@latest` to update templ -**Key Points**: -- ✅ 13+ filter fields provide exceeding flexibility -- ✅ Live preview without saving -- ✅ Search + multi-select for manual book addition -- ✅ AND/OR logic support -- ✅ Procedural TypeScript (no OOP) -- ✅ TailwindCSS classes only -- ✅ Uses shared types (BookInfo from collections.go) -- ✅ Inline onclick handlers acceptable per PROJECT_GUIDELINES.md flexibility +**Issue: Bruno tests fail** +- Verify server is running +- Check environment variables in `bruno/.env` +- Ensure test database has seed data + +#### Success Criteria + +Phase 10.6 is complete when: +- ✅ All builds succeed (Go, TypeScript, Templates) +- ✅ All Bruno tests pass +- ✅ Manual testing confirms features work +- ✅ No console errors in browser +- ✅ Dashboard loads within 2 seconds +- ✅ Custom section builder creates sections successfully +- ✅ System collection restore works + +**IMPORTANT**: Do not proceed to Phase 11 until all verification items pass. Integration issues discovered here are easier to fix before writing comprehensive unit tests. --- @@ -3976,6 +4133,178 @@ bru run --env local - ✅ `media_item_id` field present (not `id`) - ✅ Error cases handled correctly +#### 12.5 Create Collections Endpoint Tests + +**IMPORTANT**: Tests for CreateCollection with manual_book_ids support + +**File: `bruno/collections/create-collection-with-manual-books.bru`** (NEW) + +```yaml +name: Create Collection with Manual Books +meta: + group: Collections API + pre_request: Login as regular user + +req: + method: POST + url: {{baseUrl}}/api/collections + headers: + Authorization: Bearer {{token}} + Content-Type: application/json + body: + name: "Sci-Fi Favorites" + description: "My favorite sci-fi books" + icon: "🚀" + color: "#9333ea" + auto_assign_rules: + - id: rule1 + field: genre + operator: equals + value: Sci-Fi + priority: 1 + manual_book_ids: + - {{bookId1}} + - {{bookId2}} + view_settings: {} + +assertions: + - status: 201 + - jsonpath: "$.id" + exists: true + - jsonpath: "$.name" + equals: "Sci-Fi Favorites" +``` + +**File: `bruno/collections/create-collection-too-many-books.bru`** (NEW) + +```yaml +name: Create Collection - Too Many Manual Books (Validation Test) +meta: + group: Collections API + pre_request: Login as regular user + +req: + method: POST + url: {{baseUrl}}/api/collections + headers: + Authorization: Bearer {{token}} + Content-Type: application/json + body: + name: "Test Collection" + manual_book_ids: + # Generate 51 book IDs to exceed max limit + - {{bookId1}} + - {{bookId2}} + - {{bookId3}} + - {{bookId4}} + - {{bookId5}} + # ... (total of 51 IDs) + +assertions: + - status: 400 + - jsonpath: "$.error" + exists: true +``` + +**File: `bruno/collections/create-collection-invalid-book-id.bru`** (NEW) + +```yaml +name: Create Collection - Invalid Book IDs +meta: + group: Collections API + pre_request: Login as regular user + +req: + method: POST + url: {{baseUrl}}/api/collections + headers: + Authorization: Bearer {{token}} + Content-Type: application/json + body: + name: "Test Collection" + manual_book_ids: + - invalid-uuid-format + - {{bookId1}} + - another-invalid-uuid + +assertions: + - status: 201 + - jsonpath: "$.id" + exists: true + # Collection should be created, valid books added, invalid IDs skipped +``` + +**File: `bruno/collections/create-collection-rules-only.bru`** (NEW) + +```yaml +name: Create Collection - Auto-Assign Rules Only +meta: + group: Collections API + pre_request: Login as regular user + +req: + method: POST + url: {{baseUrl}}/api/collections + headers: + Authorization: Bearer {{token}} + Content-Type: application/json + body: + name: "High Rated Books" + description: "Books with rating > 4" + icon: "⭐" + color: "#FFD700" + auto_assign_rules: + - id: rule1 + field: rating + operator: greater_than + value: "4" + priority: 1 + # manual_book_ids not provided (optional field) + +assertions: + - status: 201 + - jsonpath: "$.auto_assign_rules" + exists: true +``` + +**Update Bruno test directory structure**: +``` +bruno/ +├── dashboard/ +│ ├── get-sections-success.bru +│ ├── get-sections-missing-library-id.bru +│ ├── get-sections-invalid-library-id.bru +│ ├── get-sections-unauthorized.bru +│ ├── put-preferences-success.bru +│ ├── put-preferences-unauthorized.bru +│ ├── restore-system-collection-success.bru +│ ├── restore-system-collection-invalid-name.bru +│ ├── restore-system-collection-unauthorized.bru +│ ├── preview-collection-success.bru +│ ├── preview-collection-manual-selection.bru +│ ├── preview-collection-combined.bru +│ ├── preview-collection-invalid-library.bru +│ └── preview-collection-unauthorized.bru +└── collections/ # NEW DIRECTORY + ├── create-collection-with-manual-books.bru + ├── create-collection-too-many-books.bru + ├── create-collection-invalid-book-id.bru + ├── create-collection-rules-only.bru + ├── create-collection-unauthorized.bru + └── get-collections.bru +``` + +**Run Bruno tests**: +```bash +# Test dashboard endpoints +cd bruno/dashboard +bru run --env local + +# Test collections endpoints +cd bruno/collections +bru run --env local +``` + #### 12.5 Create Collections Preview Tests **File: `bruno/dashboard/preview-collection-success.bru`** @@ -4319,7 +4648,106 @@ Key features: - AND/OR logic support for combining rules ``` -#### 13.3 User Documentation +#### 13.3 Collections API Documentation Update + +**File: `docs/developer/api/collections/create_collection.md`** (UPDATE existing) + +**Add `manual_book_ids` field to request body table:** + +```markdown +## Request Body + +| Field | Type | Required | Description | +|--------|------|-----------|-------------| +| name | string | Yes | Collection name (max 255 chars) | +| description | string | No | Collection description | +| color | string | No | Hex color code (e.g., "#FF5733") | +| icon | string | No | Emoji icon (e.g., "🚀", "📖") | +| auto_assign_rules | array | No | Array of rule objects | +| manual_book_ids | array | No | Array of book UUIDs to manually add (max 50) | +| view_settings | object | No | Per-device display preferences | +``` + +**Add validation section:** + +```markdown +## Validation + +- `manual_book_ids` array is limited to 50 items +- Returns `400 Bad Request` if more than 50 book IDs provided +- Invalid book UUIDs are skipped (don't prevent collection creation) +- Duplicate book IDs are automatically ignored (database constraint) +``` + +**Add example with manual books:** + +```markdown +### Example Request (Auto-Assign Rules + Manual Books) + +```json +{ + "name": "Sci-Fi Favorites", + "description": "My favorite sci-fi books plus manual picks", + "icon": "🚀", + "color": "#9333ea", + "auto_assign_rules": [ + { + "field": "genre", + "operator": "equals", + "value": "Sci-Fi", + "priority": 1 + } + ], + "manual_book_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "550e8400-e29b-41d4-a716-446655440001" + ], + "view_settings": { + "kobo": { + "view_mode": "grid" + } + } +} +``` + +**Notes:** +- You can combine `auto_assign_rules` AND `manual_book_ids` +- Manual books are added regardless of whether they match the auto-assign rules +- Invalid book IDs are skipped with errors logged +- Maximum 50 manual books per collection (UI constraint) +``` + +**Add error response example:** + +```markdown +### Error Responses + +| Code | Description | +|------|-------------| +| 400 | Invalid request (validation failed, > 50 manual books) | +| 400 | Invalid request (validation failed) | +| 401 | Authentication required | +| 500 | Internal server error | + +**Example: Too Many Manual Books** + +Request: +```json +{ + "name": "Test", + "manual_book_ids": [ ... 51 book IDs ... ] +} +``` + +Response (400): +```json +{ + "error": "Validation failed" +} +``` +``` + +#### 13.4 User Documentation **File: `docs/user/dashboard.md`** (UPDATE existing)