diff --git a/CAROUSEL_DASHBOARD_PLAN.md b/CAROUSEL_DASHBOARD_PLAN.md
new file mode 100644
index 0000000..864f9fd
--- /dev/null
+++ b/CAROUSEL_DASHBOARD_PLAN.md
@@ -0,0 +1,1622 @@
+# 🎬 Carousel-Style Dashboard Redesign Plan
+
+## Overview
+Transform the current dashboard into a **production-ready** horizontal carousel layout like Audiobookshelf/Kavita, with:
+- Smart sections (Continue Reading, Recently Added, etc.)
+- User collections as sections
+- Filter-based smart sections (custom collections with auto-assign rules)
+- Separate dashboard per library
+- Full accessibility, keyboard nav, and touch gestures
+- **SSR-first architecture** (data pre-populated server-side, HTMX for updates)
+
+---
+
+## 🏗️ Architecture Compliance
+
+### Project Guidelines Alignment
+
+This plan **adheres to** all PROJECT_GUIDELINES.md requirements with explicit user approval for backend modifications to improve frontend/mobile experience.
+
+**Key Compliance Points:**
+
+✅ **Full-Stack Task** (backend modifications approved):
+- Database schema changes
+- New service layer for reusable business logic
+- New API endpoints for mobile app compatibility
+- Bruno DSL tests for all new endpoints
+
+✅ **Frontend Standards**:
+- **TailwindCSS classes ONLY** - no custom CSS
+- **TypeScript ONLY** - no JavaScript files
+- **Procedural/imperative style** - no OOP (classes, inheritance, this-capture)
+- **SSR for initial data** - no AJAX on page load
+- **Progressive enhancement** - works without JavaScript
+- **HTMX for CRUD operations** (library switching, settings updates)
+
+✅ **Code Organization**:
+- **Share handler types with templates** - no duplicate type systems
+- **All business logic in services** - reusable for SSR/API/mobile
+- **Minimal project structure changes** - contextually appropriate directories
+
+✅ **Database Operations**:
+- **Merge into existing schema.sql** - no migration files
+- **Atomic schema changes** - complete success or rejection
+- **Pre-production app** - database will be recreated after schema changes
+- **pgx v5 standards** - proper connection handling
+
+✅ **API Documentation**:
+- **Bruno .bru files** for all new endpoints
+- **Three-context testing** (no user, user, admin)
+- **Backward compatibility** for mobile apps
+- **docs/developer/api/** documentation updates
+
+---
+
+## 📋 Implementation Plan
+
+### **Phase 1: Database Schema Changes** (2-3 hours)
+
+#### 1.1 Update Schema File (Not Migrations)
+**File: `database/schema/schema.sql`** (MODIFY existing file)
+
+**CRITICAL**: This is a pre-production app. After updating schema.sql, recreate database:
+```bash
+podman compose down -v # Delete volumes (loses all data)
+podman compose up -d # Start fresh with new schema
+```
+
+**Add to schema.sql**:
+
+```sql
+-- Table: user_dashboard_preferences
+CREATE TABLE user_dashboard_preferences (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ library_id UUID REFERENCES libraries(id) ON DELETE CASCADE,
+ hidden_sections TEXT[] DEFAULT '{}',
+ section_order TEXT[] DEFAULT '{}',
+ items_per_section INT DEFAULT 20,
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW(),
+ UNIQUE(user_id, library_id)
+);
+
+-- Index for fast lookups
+CREATE INDEX idx_dashboard_prefs_user_library ON user_dashboard_preferences(user_id, library_id);
+
+-- Add column to existing collections table
+ALTER TABLE collections ADD COLUMN IF NOT EXISTS show_on_dashboard BOOLEAN DEFAULT false;
+
+-- Index for dashboard queries
+CREATE INDEX IF NOT EXISTS idx_collections_dashboard ON collections(user_id, show_on_dashboard)
+ WHERE show_on_dashboard = true;
+
+-- Predefined smart sections (system-level, not user-created)
+CREATE TABLE smart_section_types (
+ id SERIAL PRIMARY KEY,
+ section_key TEXT UNIQUE NOT NULL,
+ title TEXT NOT NULL,
+ description TEXT,
+ icon TEXT,
+ default_priority INT,
+ is_global BOOLEAN DEFAULT false -- true = uses global data (Recently Added), false = per-user
+);
+
+-- Insert default sections
+INSERT INTO smart_section_types (section_key, title, description, icon, default_priority, is_global) VALUES
+('continue-reading', 'Continue Reading', 'Books you''re currently reading', '📖', 1, false),
+('in-progress', 'In Progress', 'Books you''ve started but not finished', '📚', 2, false),
+('recently-added', 'Recently Added', 'Newly added items to this library', '🆕', 3, true),
+('recently-read', 'Recently Read', 'Books you''ve finished', '✅', 4, false),
+('unread', 'Not Started', 'Books you haven''t read yet', '📕', 5, false);
+```
+
+#### 1.2 Regenerate Database Code
+```bash
+cd internal/database
+sqlc generate
+```
+
+Verify:
+- ✅ `models.go` has new structs
+- ✅ `queries.sql` is ready for new queries
+- ✅ No compilation errors
+
+---
+
+### **Phase 2: Service Layer** (3-4 hours)
+
+**File: `internal/services/dashboard_service.go`** (new file)
+
+**COMPLIANCE**: All business logic in reusable service (per guidelines)
+
+```go
+package services
+
+import (
+ "context"
+ "bookhoard/internal/database"
+ "github.com/google/uuid"
+)
+
+type DashboardService struct {
+ db *database.Queries
+}
+
+// Section data - use handler types, not template types
+type Section struct {
+ ID string `json:"id"`
+ Type string `json:"type"` // "smart", "collection"
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Icon string `json:"icon"`
+ Items []MediaItem `json:"items"`
+ ViewAllURL string `json:"view_all_url"`
+ Priority int `json:"priority"`
+ IsHidden bool `json:"is_hidden"`
+}
+
+// MediaItem - REUSE existing handler type
+type MediaItem = database.MediaItem
+
+// NewDashboardService creates service instance
+func NewDashboardService(db *database.Queries) *DashboardService {
+ return &DashboardService{db: db}
+}
+
+// GetSections fetches all sections for dashboard
+// REUSABLE by SSR handlers, API endpoints, mobile apps
+func (s *DashboardService) GetSections(ctx context.Context, userID, libraryID uuid.UUID) ([]Section, error) {
+ // 1. Get user preferences
+ prefs, _ := s.db.GetDashboardPreferences(ctx, database.GetDashboardPreferencesParams{
+ UserID: database.SetUUID(userID),
+ LibraryID: database.SetUUID(libraryID),
+ })
+
+ // 2. Get smart sections (use existing progress API)
+ smartSections := s.getSmartSections(ctx, userID, libraryID, prefs)
+
+ // 3. Get user collections marked for dashboard
+ collectionSections := s.getCollectionSections(ctx, userID, libraryID, prefs)
+
+ // 4. Merge and sort by priority/user order
+ return s.mergeAndSortSections(smartSections, collectionSections, prefs)
+}
+
+func (s *DashboardService) getSmartSections(ctx context.Context, userID, libraryID uuid.UUID, prefs database.UserDashboardPreferences) []Section {
+ // Use EXISTING APIs:
+ // - s.db.GetUniversalProgress for Continue Reading, In Progress, Recently Read
+ // - s.db.ListMediaItems for Recently Added
+ // NO direct database access - use queries
+}
+
+func (s *DashboardService) getCollectionSections(ctx context.Context, userID, libraryID uuid.UUID, prefs database.UserDashboardPreferences) []Section {
+ // Query collections WHERE show_on_dashboard = true
+ // For each collection, fetch items using existing GetCollectionItems
+}
+
+func (s *DashboardService) mergeAndSortSections(smart, collections []Section, prefs database.UserDashboardPreferences) []Section {
+ // Merge by priority or user's section_order preference
+}
+```
+
+**Key Points**:
+- ✅ Service layer holds all business logic
+- ✅ Reusable by SSR, API, mobile
+- ✅ No direct database access from handlers
+- ✅ Uses existing database queries
+- ✅ Procedural/imperative style (no OOP)
+
+---
+
+### **Phase 3: Database Queries** (1-2 hours)
+
+**File: `internal/database/queries/queries.sql`** (ADD to existing file)
+
+```sql
+-- name: GetDashboardPreferences :one
+SELECT * FROM user_dashboard_preferences
+WHERE user_id = $1 AND library_id = $2;
+
+-- name: UpsertDashboardPreferences :one
+INSERT INTO user_dashboard_preferences (user_id, library_id, hidden_sections, section_order, items_per_section)
+VALUES ($1, $2, $3, $4, $5)
+ON CONFLICT (user_id, library_id)
+DO UPDATE SET
+ hidden_sections = EXCLUDED.hidden_sections,
+ section_order = EXCLUDED.section_order,
+ items_per_section = EXCLUDED.items_per_section,
+ updated_at = NOW()
+RETURNING *;
+
+-- name: UpdateDashboardPreferences :one
+UPDATE user_dashboard_preferences
+SET hidden_sections = $2,
+ section_order = $3,
+ items_per_section = $4,
+ updated_at = NOW()
+WHERE user_id = $1 AND library_id = $5
+RETURNING *;
+
+-- name: GetCollectionsForDashboard :many
+SELECT c.* FROM collections c
+WHERE c.user_id = $1
+ AND c.show_on_dashboard = true
+ORDER BY c.created_at DESC;
+
+-- name: SetCollectionDashboardVisibility :one
+INSERT INTO collections (id, show_on_dashboard)
+VALUES ($1, $2)
+ON CONFLICT (id) DO UPDATE SET
+ show_on_dashboard = EXCLUDED.show_on_dashboard
+RETURNING *;
+```
+
+Regenerate: `cd internal/database && sqlc generate`
+
+---
+
+### **Phase 4: HTTP Handlers** (2-3 hours)
+
+**File: `internal/handlers/dashboard.go`** (new file)
+
+**COMPLIANCE**: Thin handlers, all logic in service layer
+
+```go
+package handlers
+
+import (
+ "bookhoard/internal/services"
+ "github.com/labstack/echo/v4"
+)
+
+type DashboardHandler struct {
+ db *database.Queries
+ dashboardSvc *services.DashboardService
+}
+
+// NewDashboardHandler creates handler instance
+func NewDashboardHandler(db *database.Queries) *DashboardHandler {
+ return &DashboardHandler{
+ db: db,
+ dashboardSvc: services.NewDashboardService(db),
+ }
+}
+
+// GetDashboard renders full dashboard with pre-populated data (SSR)
+func (h *DashboardHandler) GetDashboard(c echo.Context) error {
+ user := MustGetAuthenticatedUser(c)
+ libraryID := h.getSelectedLibrary(c, user.ID)
+
+ // CALL SERVICE (not database directly)
+ sections, err := h.dashboardSvc.GetSections(c.Request().Context(), user.ID, libraryID)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "failed to load dashboard")
+ }
+
+ libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), user.ID)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "failed to load libraries")
+ }
+
+ // SSR - pre-populate all data, no client-side API calls
+ // Use handler types directly (database.Library, etc.) not template types
+ return c.Render(http.StatusOK, "dashboard", map[string]interface{}{
+ "User": user,
+ "Sections": sections,
+ "Libraries": libraries,
+ "CurrentLibraryID": libraryID,
+ })
+}
+
+// GetDashboardSections returns partial HTML for HTMX swap
+func (h *DashboardHandler) GetDashboardSections(c echo.Context) error {
+ user := MustGetAuthenticatedUser(c)
+ libraryID, _ := uuid.Parse(c.QueryParam("library_id"))
+
+ sections, err := h.dashboardSvc.GetSections(c.Request().Context(), user.ID, libraryID)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ // Return partial template for HTMX
+ return c.Render(http.StatusOK, "dashboard-sections-partial", sections)
+}
+
+// UpdateDashboardPreferences handles settings updates (HTMX POST)
+func (h *DashboardHandler) UpdateDashboardPreferences(c echo.Context) error {
+ user := MustGetAuthenticatedUser(c)
+
+ var req struct {
+ LibraryID string `json:"library_id"`
+ HiddenSections []string `json:"hidden_sections"`
+ SectionOrder []string `json:"section_order"`
+ ItemsPerSection int `json:"items_per_section"`
+ }
+
+ if err := c.Bind(&req); err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
+ }
+
+ // Update via service (through database queries)
+ // ...
+}
+```
+
+**Routes to add to `internal/router/router.go`**:
+```go
+// Dashboard routes
+dashboard := e.Group("/dashboard")
+dashboard.GET("", cfg.DashboardHandler.GetDashboard)
+dashboard.GET("/sections", cfg.DashboardHandler.GetDashboardSections) // HTMX
+dashboard.POST("/preferences", cfg.DashboardHandler.UpdateDashboardPreferences) // HTMX/API
+
+// API for mobile apps
+apiDashboard := protected.Group("/api/dashboard")
+apiDashboard.GET("/sections", cfg.DashboardHandler.GetDashboardSectionsAPI) // JSON
+```
+
+---
+
+### **Phase 5: Bruno API Tests** (1 hour)
+
+**COMPLIANCE**: All new endpoints need Bruno DSL tests
+
+**File: `bruno/dashboard/get-dashboard-sections.bru`** (new file)
+
+```bruno
+{
+ "meta": {
+ "type": "http",
+ "name": "Get Dashboard Sections",
+ "seq": 1
+ },
+ "req": {
+ "method": "GET",
+ "url": "{{baseUrl}}/api/dashboard/sections?library_id={{libraryId}}",
+ "headers": [
+ {
+ "name": "Authorization",
+ "value": "Bearer {{userToken}}"
+ }
+ ]
+ },
+ "tests": {
+ "no_user": { "status": 401 },
+ "user": { "status": 200, "has": "sections" },
+ "admin": { "status": 200, "has": "sections" }
+ }
+}
+```
+
+Create tests for:
+1. ✅ `GET /api/dashboard/sections` (no user, user, admin)
+2. ✅ `POST /api/dashboard/preferences` (user, admin)
+3. ✅ Verify backward compatibility
+
+---
+
+### **Phase 6: Templates** (4-5 hours)
+
+**COMPLIANCE**:
+- ✅ Use TailwindCSS classes ONLY (no custom CSS)
+- ✅ Share handler types (no template.*Data types)
+- ✅ SSR for initial data
+- ✅ HTMX for updates
+
+#### 6.1 Main Dashboard Template
+**File: `templates/dashboard.templ`** (REPLACE existing)
+
+```templ
+package templates
+
+import (
+ "bookhoard/internal/handlers"
+)
+
+// Use handler types directly
+templ Dashboard(user handlers.User, sections []services.Section, libraries []handlers.LibraryData, currentLibraryID string) {
+
+
+
+
+
+ Dashboard - Bookhoard
+
+
+
+
+
+
+ @Header(user, "/dashboard")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ for _, section := range sections {
+ @SectionCarousel(section)
+ }
+
+
+
+ @DashboardSettingsModal(sections)
+
+
+
+
+}
+```
+
+#### 6.2 Section Carousel Component
+**File: `templates/components.templ`** (new file)
+
+```templ
+package templates
+
+templ SectionCarousel(section services.Section) {
+
+
+
+
+
{ section.Icon }
+
+
{ section.Title }
+ if section.Description != "" {
+
{ section.Description }
+ }
+
+
+
+
+ View All →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+templ BookCard(item handlers.MediaItem) {
+
+}
+
+templ DashboardSettingsModal(sections []services.Section) {
+
+
+
+
Customize Dashboard
+
+
+
+
+ Drag to reorder sections, toggle visibility with the switch.
+
+
+
+
+ for _, section := range sections {
+
+
+ ☰
+ { section.Icon }
+ { section.Title }
+
+
+
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+```
+
+#### 6.3 HTMX Partial Template
+**File: `templates/dashboard_sections_partial.templ`** (new file)
+
+```templ
+package templates
+
+templ DashboardSectionsPartial(sections []services.Section) {
+ for _, section := range sections {
+ @SectionCarousel(section)
+ }
+}
+```
+
+---
+
+### **Phase 7: TypeScript (Procedural)** (4-5 hours)
+
+**COMPLIANCE**:
+- ✅ TypeScript ONLY (no .js files)
+- ✅ Procedural/imperative style (no classes, no OOP)
+- ✅ Progressive enhancement (works without JS)
+- ✅ HTMX for updates
+
+#### 7.1 Carousel Interactions
+**File: `web/src/carousel.ts`** (new file)
+
+```typescript
+// Procedural style - no classes, no OOP
+// Functions that operate on DOM elements
+
+const SCROLL_AMOUNT = 300;
+
+export function scrollCarousel(sectionId: string, direction: number): void {
+ const track = document.getElementById(`carousel-track-${sectionId}`);
+ if (!track) return;
+
+ const scrollAmount = direction * SCROLL_AMOUNT;
+ track.scrollBy({ left: scrollAmount, behavior: 'smooth' });
+}
+
+export function initializeCarousels(): void {
+ // Add touch/swipe support
+ const tracks = document.querySelectorAll('.carousel-track');
+
+ tracks.forEach(track => {
+ let isDown = false;
+ let startX: number;
+ let scrollLeft: number;
+
+ track.addEventListener('mousedown', (e: MouseEvent) => {
+ isDown = true;
+ startX = e.pageX - (track as HTMLElement).offsetLeft;
+ scrollLeft = track.scrollLeft;
+ });
+
+ track.addEventListener('mouseleave', () => {
+ isDown = false;
+ });
+
+ track.addEventListener('mouseup', () => {
+ isDown = false;
+ });
+
+ track.addEventListener('mousemove', (e: MouseEvent) => {
+ if (!isDown) return;
+ e.preventDefault();
+ const x = e.pageX - (track as HTMLElement).offsetLeft;
+ const walk = (x - startX) * 2;
+ track.scrollLeft = scrollLeft - walk;
+ });
+
+ // Touch events for mobile
+ track.addEventListener('touchstart', (e: TouchEvent) => {
+ startX = e.touches[0].pageX - (track as HTMLElement).offsetLeft;
+ scrollLeft = track.scrollLeft;
+ });
+
+ track.addEventListener('touchmove', (e: TouchEvent) => {
+ const x = e.touches[0].pageX - (track as HTMLElement).offsetLeft;
+ const walk = (x - startX) * 2;
+ track.scrollLeft = scrollLeft - walk;
+ });
+ });
+}
+
+// Initialize on DOM ready
+document.addEventListener('DOMContentLoaded', initializeCarousels);
+```
+
+#### 7.2 Dashboard Settings
+**File: `web/src/dashboard-settings.ts`** (new file)
+
+```typescript
+// Procedural functions for modal and settings
+
+export function openDashboardSettings(): void {
+ const modal = document.getElementById('dashboard-settings-modal');
+ if (modal) {
+ modal.classList.remove('hidden');
+ initializeDragAndDrop();
+ }
+}
+
+export function closeDashboardSettings(): void {
+ const modal = document.getElementById('dashboard-settings-modal');
+ if (modal) {
+ modal.classList.add('hidden');
+ }
+}
+
+function initializeDragAndDrop(): void {
+ const list = document.getElementById('section-list');
+ if (!list) return;
+
+ const items = list.querySelectorAll('.section-item');
+
+ items.forEach(item => {
+ item.addEventListener('dragstart', handleDragStart);
+ item.addEventListener('dragover', handleDragOver);
+ item.addEventListener('drop', handleDrop);
+ item.addEventListener('dragend', handleDragEnd);
+ });
+}
+
+function handleDragStart(e: DragEvent): void {
+ const target = e.target as HTMLElement;
+ target.style.opacity = '0.5';
+}
+
+function handleDragOver(e: DragEvent): void {
+ e.preventDefault();
+}
+
+function handleDrop(e: DragEvent): void {
+ e.preventDefault();
+ const target = e.target as HTMLElement;
+ // Reorder logic...
+}
+
+function handleDragEnd(e: DragEvent): void {
+ const target = e.target as HTMLElement;
+ target.style.opacity = '1';
+}
+
+export function toggleSectionVisibility(sectionId: string): void {
+ // Update local state, save on submit
+}
+
+export function saveDashboardSettings(): void {
+ const sectionList = document.getElementById('section-list');
+ const items = sectionList?.querySelectorAll('.section-item');
+
+ const sectionOrder: string[] = [];
+ const hiddenSections: string[] = [];
+
+ items?.forEach(item => {
+ const id = item.getAttribute('data-section-id');
+ if (!id) return;
+
+ sectionOrder.push(id);
+
+ const checkbox = item.querySelector('input[type="checkbox"]');
+ if (checkbox && !(checkbox as HTMLInputElement).checked) {
+ hiddenSections.push(id);
+ }
+ });
+
+ const data = {
+ library_id: getCurrentLibraryId(),
+ hidden_sections: hiddenSections,
+ section_order: sectionOrder,
+ items_per_section: parseInt((document.getElementById('items-count-display') as HTMLElement).textContent)
+ };
+
+ fetch('/api/dashboard/preferences', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data)
+ })
+ .then(response => response.json())
+ .then(() => {
+ closeDashboardSettings();
+ location.reload(); // Or HTMX refresh
+ })
+ .catch(error => {
+ console.error('Failed to save settings:', error);
+ showToast('Failed to save settings', 'error');
+ });
+}
+
+function getCurrentLibraryId(): string {
+ const select = document.getElementById('library-select') as HTMLSelectElement;
+ return select?.value || '';
+}
+```
+
+**Build setup**:
+```json
+// package.json - add TypeScript build
+{
+ "scripts": {
+ "build:carousel": "esbuild web/src/carousel.ts --bundle --minify --outfile=web/static/carousel.js",
+ "build:dashboard-settings": "esbuild web/src/dashboard-settings.ts --bundle --minify --outfile=web/static/dashboard-settings.js"
+ }
+}
+```
+
+---
+
+### **Phase 8: Book Detail Page** (3-4 hours)
+
+**File: `templates/book_detail.templ`** (new file)
+
+**COMPLIANCE**: Use handler types, TailwindCSS, SSR
+
+```templ
+package templates
+
+templ BookDetail(user handlers.User, book handlers.MediaItem, progress handlers.ReadingProgress, rating float64, collections []handlers.CollectionData) {
+
+
+
+
+
+ { book.Title } - Bookhoard
+
+
+
+
+
+
+ @Header(user, "")
+
+
+
+
+
+
+
+
+
+ @if book.CoverImagePath.Valid {
+

+ } @else {
+

+ }
+
+
+
+
+
+
+ { book.Title }
+
+ @if book.Author.Valid {
+
+ by { book.Author.String }
+
+ }
+
+
+ @if progress.Percentage > 0 {
+
+
+ Reading Progress
+
+ { fmt.Sprintf("%.0f%%", progress.Percentage) }
+
+
+
+
+ }
+
+
+
+
+
+
+
+
+
+
Details
+
+ @if book.Series.Valid {
+
+ | Series |
+ { book.Series.String } |
+
+ }
+ @if book.Genre.Valid {
+
+ | Genre |
+ { book.Genre.String } |
+
+ }
+
+ | Pages |
+ { book.PageCount.Int32 } |
+
+
+
+
+
+ @if book.Description.Valid {
+
+
Synopsis
+
+ { book.Description.String }
+
+
+ }
+
+
+
+
+
+
+
+}
+```
+
+---
+
+### **Phase 9: Documentation** (1-2 hours)
+
+**COMPLIANCE**: Update documentation per guidelines
+
+#### 9.1 User Documentation
+**File: `docs/user/dashboard.md`** (new file)
+
+```markdown
+# Dashboard
+
+The Bookhoard dashboard provides a Carousel-style horizontal carousel interface for browsing your book library.
+
+## Sections
+
+### Smart Sections
+
+**Continue Reading**: Books you're currently reading, sorted by last read time.
+
+**In Progress**: Books you've started but haven't finished.
+
+**Recently Added**: Newest items added to your library (global across all users).
+
+**Recently Read**: Books you've completed (100% progress).
+
+**Not Started**: Books with no reading progress.
+
+### User Collections
+
+Any collection marked with "Show on Dashboard" will appear as a section.
+
+### Customizing Your Dashboard
+
+1. Click the ⚙️ (gear icon) in the top-right
+2. Drag sections to reorder
+3. Toggle visibility with switches
+4. Adjust items per section (10-50)
+5. Click "Save Changes"
+
+### Library Switching
+
+Use the dropdown in the sticky header to switch between libraries. Settings are per-library.
+```
+
+#### 9.2 API Documentation
+**File: `docs/developer/api/dashboard/sections.md`** (new file)
+
+```markdown
+# Get Dashboard Sections
+
+Returns all dashboard sections for the specified library.
+
+## Endpoint
+
+`GET /api/dashboard/sections`
+
+## Query Parameters
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| library_id | string | Yes | Library UUID |
+
+## Response
+
+```json
+{
+ "sections": [
+ {
+ "id": "continue-reading",
+ "type": "smart",
+ "title": "Continue Reading",
+ "icon": "📖",
+ "items": [...],
+ "view_all_url": "/section/continue-reading"
+ }
+ ]
+}
+```
+
+## Examples
+
+See `bruno/dashboard/get-dashboard-sections.bru`
+```
+
+---
+
+### **Phase 10: Testing & Verification** (2-3 hours)
+
+**COMPLIANCE**: Follow testing guidelines from test_helpers.go
+
+#### 10.1 Integration Tests
+**File: `cmd/server/tests/dashboard_test.go`** (new file)
+
+**Pattern**: Call `setupTestServer(t)` ONCE, use `t.Run()` for subtests
+
+```go
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestDashboardSections tests the dashboard sections endpoint
+func TestDashboardSections(t *testing.T) {
+ setup := setupTestServer(t) // ✅ Called ONCE per test function
+ client := &http.Client{}
+
+ // Get admin token
+ adminToken := loginTestUser(t, setup.Server, setup.DB)
+
+ // Create test library with media items
+ deviceSetup := setupDeviceTest(t)
+ libraryID := deviceSetup.CreateLibrary(t, "Test Ebooks Library", "ebooks")
+
+ t.Run("GetSections_WithoutAuth", func(t *testing.T) {
+ req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID, nil)
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+ })
+
+ t.Run("GetSections_WithAdminUser", func(t *testing.T) {
+ req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID, nil)
+ req.Header.Set("Authorization", "Bearer "+adminToken)
+
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var result map[string]interface{}
+ json.NewDecoder(resp.Body).Decode(&result)
+
+ sections, ok := result["sections"].([]interface{})
+ assert.True(t, ok, "Should have sections array")
+ assert.GreaterOrEqual(t, len(sections), 5, "Should have at least 5 smart sections")
+ })
+
+ t.Run("GetSections_WithRegularUser", func(t *testing.T) {
+ // Get regular user token
+ regularToken := loginRegularUser(t, setup.Server, setup.DB)
+
+ req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID, nil)
+ req.Header.Set("Authorization", "Bearer "+regularToken)
+
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var result map[string]interface{}
+ json.NewDecoder(resp.Body).Decode(&result)
+
+ sections, ok := result["sections"].([]interface{})
+ assert.True(t, ok, "Should have sections array")
+ assert.GreaterOrEqual(t, len(sections), 5, "Should have at least 5 smart sections")
+ })
+
+ t.Run("GetSections_InvalidLibraryID", func(t *testing.T) {
+ req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id=invalid-uuid", nil)
+ req.Header.Set("Authorization", "Bearer "+adminToken)
+
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
+ })
+}
+
+// TestDashboardPreferences tests the preferences endpoints
+func TestDashboardPreferences(t *testing.T) {
+ setup := setupTestServer(t)
+ token := loginTestUser(t, setup.Server, setup.DB)
+ client := &http.Client{}
+
+ // Create test library
+ deviceSetup := setupDeviceTest(t)
+ libraryID := deviceSetup.CreateLibrary(t, "Test Library", "ebooks")
+
+ t.Run("UpdatePreferences_Valid", func(t *testing.T) {
+ reqBody := map[string]interface{}{
+ "library_id": libraryID,
+ "hidden_sections": []string{"recently-added"},
+ "section_order": []string{"continue-reading", "in-progress"},
+ "items_per_section": 25,
+ }
+ body, _ := json.Marshal(reqBody)
+
+ req, _ := http.NewRequest("POST", setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ })
+
+ t.Run("UpdatePreferences_WithoutAuth", func(t *testing.T) {
+ reqBody := map[string]interface{}{
+ "library_id": libraryID,
+ }
+ body, _ := json.Marshal(reqBody)
+
+ req, _ := http.NewRequest("POST", setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ // No authorization header
+
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+ })
+}
+
+// TestDashboardCollectionVisibility tests collection show_on_dashboard functionality
+func TestDashboardCollectionVisibility(t *testing.T) {
+ setup := setupTestServer(t)
+ token := loginTestUser(t, setup.Server, setup.DB)
+
+ t.Run("CreateCollection_WithDashboardVisibility", func(t *testing.T) {
+ deviceSetup := setupDeviceTest(t)
+
+ reqBody := map[string]interface{}{
+ "name": "Test Dashboard Collection",
+ "description": "A collection for testing dashboard",
+ "color": "#FF5733",
+ "icon": "📚",
+ }
+ body, _ := json.Marshal(reqBody)
+
+ req, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusCreated, resp.StatusCode)
+
+ var result map[string]interface{}
+ json.NewDecoder(resp.Body).Decode(&result)
+
+ collectionID := result["id"].(string)
+
+ // Update to show on dashboard
+ updateReq := map[string]interface{}{
+ "show_on_dashboard": true,
+ }
+ updateBody, _ := json.Marshal(updateReq)
+
+ updateHTTP, _ := http.NewRequest("PUT", setup.Server.URL+"/api/collections/"+collectionID, bytes.NewBuffer(updateBody))
+ updateHTTP.Header.Set("Content-Type", "application/json")
+ updateHTTP.Header.Set("Authorization", "Bearer "+token)
+
+ updateResp, err := http.DefaultClient.Do(updateHTTP)
+ require.NoError(t, err)
+ defer updateResp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, updateResp.StatusCode)
+
+ // Verify it appears in dashboard sections
+ sectionsReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections", nil)
+ sectionsReq.Header.Set("Authorization", "Bearer "+token)
+
+ sectionsResp, err := http.DefaultClient.Do(sectionsReq)
+ require.NoError(t, err)
+ defer sectionsResp.Body.Close()
+
+ var sectionsResult map[string]interface{}
+ json.NewDecoder(sectionsResp.Body).Decode(§ionsResult)
+
+ sections := sectionsResult["sections"].([]interface{})
+ // Should include our collection
+ assert.Greater(t, len(sections), 5, "Should have smart sections + collection")
+ })
+}
+```
+
+#### 10.2 Bruno API Tests
+
+**Files Created** (`bruno/dashboard/`):
+
+All Bruno files follow proper format (matching existing project files):
+- ✅ Proper `meta` block with `name` (NO quotes around name value)
+- ✅ Valid method blocks (get/post/put) with url, headers, body/query
+- ✅ Tests block with `test()` function syntax (not assert blocks)
+- ✅ Snake_case variables from environment (`base_url`, `user_token`, `library_id`)
+- ✅ Proper `auth: inherit` for authenticated endpoints
+- ✅ Balanced braces
+- ✅ `docs` block with API documentation
+
+**File Examples**:
+
+**`get-dashboard-sections.bru`**:
+```bru
+meta {
+ name: Get Dashboard Sections
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{base_url}}/api/dashboard/sections
+ body: none
+ auth: inherit
+ query: {
+ library_id: "{{library_id}}"
+ }
+}
+
+tests {
+ test("status must be 200 with auth", function() {
+ expect(res.status).to.eql(200);
+ });
+
+ test("response has sections array", function() {
+ const body = JSON.parse(res.body);
+ expect(body).to.have.property("sections");
+ expect(body.sections).to.be.an("array");
+ });
+}
+```
+
+**Key Bruno Requirements** (matching existing files):
+- ❌ `name: "Get Dashboard"` (wrong - has quotes)
+- ✅ `name: Get Dashboard` (correct - no quotes)
+- ❌ `{{baseUrl}}` (wrong - camelCase)
+- ✅ `{{base_url}}` (correct - snake_case)
+- ❌ `assert { assertions: [...] }` (wrong - old format)
+- ✅ `test("name", function() { expect(...).to.eql(...) })` (correct)
+- ✅ Use `auth: inherit` instead of manual Authorization headers
+- ✅ Include `docs` block with API documentation
+- ✅ Include `settings` block with `encodeUrl: true` and `timeout: 0`
+
+**Complete Bruno Files Created**:
+1. `get-dashboard-sections.bru` - Get sections for library
+2. `update-preferences.bru` - Update user dashboard preferences
+3. `get-sections-by-library.bru` - Filter sections by library
+4. `create-collection-with-dashboard.bru` - Create collection with `show_on_dashboard: true`
+5. `update-collection-visibility.bru` - Toggle collection dashboard visibility
+
+**All 5 files validated successfully** ✅
+
+#### 10.3 Important: Test Helper Guidelines
+
+**From PROJECT_GUIDELINES.md**:
+
+✅ **ALWAYS use `setupTestServer()` helper**:
+- Call it ONCE per test function (not in subtests)
+- Uses `max_conns=1` to prevent connection exhaustion
+- Automatic cleanup via `t.Cleanup()` (no defer needed)
+- Returns `*TestServerSetup` with DB, Server, Config
+
+❌ **NEVER create separate database pools per test**:
+- 78 tests × 4 connections (default) = 312 connections > PostgreSQL's 100 limit
+- That's why we use `max_conns=1` in test configuration
+
+✅ **Share one test setup across all subtests**:
+```go
+func TestDashboard(t *testing.T) {
+ setup := setupTestServer(t) // ✅ ONCE
+ token := loginTestUser(t, setup.Server, setup.DB)
+
+ t.Run("Subtest1", func(t *testing.T) { /* use setup */ })
+ t.Run("Subtest2", func(t *testing.T) { /* use setup */ })
+}
+```
+
+❌ **NEVER call setupTestServer() in loops**:
+```go
+// ❌ WRONG - creates multiple DB pools
+for _, tc := range cases {
+ setup := setupTestServer(t) // DON'T DO THIS
+}
+```
+
+✅ **DO**:
+```go
+func TestFeature(t *testing.T) {
+ setup := setupTestServer(t) // ✅ ONCE per function
+ token := loginTestUser(t, setup.Server, setup.DB)
+
+ t.Run("Subtest1", func(t *testing.T) {
+ // Use setup, token
+ })
+
+ t.Run("Subtest2", func(t *testing.T) {
+ // Use same setup, token
+ })
+}
+```
+
+❌ **DON'T**:
+```go
+func TestFeature(t *testing.T) {
+ t.Run("Subtest1", func(t *testing.T) {
+ setup := setupTestServer(t) // ❌ Creates extra DB connections
+ })
+
+ t.Run("Subtest2", func(t *testing.T) {
+ setup := setupTestServer(t) // ❌ Exhausts connection pool
+ })
+}
+```
+
+**Three-Context Testing**:
+```go
+t.Run("WithoutAuth", func(t *testing.T) {
+ // No Authorization header → expect 401
+})
+
+t.Run("WithRegularUser", func(t *testing.T) {
+ token := loginRegularUser(t, setup.Server, setup.DB)
+ // Regular user context → expect 200/403 depending on endpoint
+})
+
+t.Run("WithAdmin", func(t *testing.T) {
+ token := loginTestUser(t, setup.Server, setup.DB)
+ // Admin context → expect 200
+})
+```
+
+#### 10.4 Verification Checklist
+
+Before committing:
+```bash
+# 1. Run verification script
+bash scripts/verify-guidelines.sh
+
+# 2. Build affected packages
+go build ./internal/handlers
+go build ./internal/services
+go build ./templates
+
+# 3. Run tests
+go test ./cmd/server/tests/... -v -run TestDashboard
+
+# 4. Check for TypeScript
+# No .js files allowed, only .ts
+
+# 5. Check for custom CSS
+# Should only use TailwindCSS classes
+
+# 6. Verify docs render
+# Visit /docs endpoint and search for "dashboard"
+```
+
+---
+
+## 🗂️ File Structure Summary
+
+```
+Modified Files (COMPLIANT with guidelines):
+├── database/schema/schema.sql (Add tables, NO migration files)
+├── internal/database/queries/queries.sql (Add dashboard queries)
+├── internal/router/router.go (Add dashboard routes)
+└── templates/dashboard.templ (Replace with SSR version)
+
+New Files:
+├── internal/services/dashboard_service.go (Reusable business logic)
+├── internal/handlers/dashboard.go (Thin handlers, no logic)
+├── templates/components.templ (Carousel, modal components)
+├── templates/dashboard_sections_partial.templ (HTMX partial)
+├── templates/book_detail.templ (Book detail page)
+├── web/src/carousel.ts (Procedural TypeScript)
+├── web/src/dashboard-settings.ts (Procedural TypeScript)
+├── cmd/server/tests/dashboard_test.go (Integration tests using test_helpers)
+├── bruno/dashboard/get-dashboard-sections.bru (API test)
+├── bruno/dashboard/update-preferences.bru (API test)
+├── docs/user/dashboard.md (User documentation)
+└── docs/developer/api/dashboard/sections.md (API reference)
+```
+
+---
+
+## ⏱️ Time Estimate Summary
+
+| Phase | Description | Time |
+|-------|-------------|------|
+| 1 | Database schema changes (schema.sql, no migrations) | 2-3 hrs |
+| 2 | Service layer (reusable for SSR/API/mobile) | 3-4 hrs |
+| 3 | Database queries | 1-2 hrs |
+| 4 | HTTP handlers (thin, logic in services) | 2-3 hrs |
+| 5 | Bruno API tests (3 contexts) | 1 hr |
+| 6 | Templates (TailwindCSS only, SSR, handler types) | 4-5 hrs |
+| 7 | TypeScript (procedural, no OOP) | 4-5 hrs |
+| 8 | Book detail page | 3-4 hrs |
+| 9 | Documentation (docs/user, docs/developer/api) | 1-2 hrs |
+| 10 | Testing & verification | 2-3 hrs |
+| | **Total** | **23-30 hrs** |
+
+---
+
+## 🎯 Implementation Order (Sprint Structure)
+
+**Sprint 1** (Foundation - Backend First):
+1. Phase 1: Database schema changes
+2. Phase 3: Database queries
+3. Phase 2: Service layer (testable independently)
+
+**Sprint 2** (Handlers & Tests):
+4. Phase 4: HTTP handlers
+5. Phase 5: Bruno API tests
+6. Phase 9: Documentation
+
+**Sprint 3** (Frontend):
+7. Phase 6: Templates (dashboard, components)
+8. Phase 8: Book detail page
+
+**Sprint 4** (Interactivity):
+9. Phase 7: TypeScript (carousel, settings)
+10. Phase 10: Testing & verification
+
+---
+
+## 🎨 Smart Sections Definitions
+
+| Section Key | Title | Icon | Data Source | Global? | View All URL |
+|-------------|-------|------|-------------|---------|-------------|
+| `continue-reading` | Continue Reading | 📖 | `GET /api/progress` | ❌ | `/section/continue-reading` |
+| `in-progress` | In Progress | 📚 | `GET /api/progress` | ❌ | `/section/in-progress` |
+| `recently-added` | Recently Added | 🆕 | `GET /api/media-items` | ✅ | `/section/recently-added` |
+| `recently-read` | Recently Read | ✅ | `GET /api/progress` | ❌ | `/history` |
+| `unread` | Not Started | 📕 | Media items LEFT JOIN progress WHERE null | ❌ | `/section/unread` |
+
+---
+
+## ✅ Pre-Commit Checklist
+
+Before committing, verify:
+
+**Backend**:
+- [ ] Schema changes merged into `database/schema/schema.sql` (NO migration files)
+- [ ] Regenerated database code with `sqlc generate`
+- [ ] All business logic in `services/` (not handlers)
+- [ ] Bruno .bru files created for all new endpoints
+- [ ] Tests in `cmd/server/tests/dashboard_test.go` using setupTestServer() helper
+- [ ] Tests cover 3 contexts (no user, regular user, admin)
+- [ ] `setupTestServer(t)` called ONCE per test function (not in subtests)
+- [ ] `go build ./...` succeeds
+- [ ] `go test ./cmd/server/tests/... -v -run TestDashboard` passes
+
+**Frontend**:
+- [ ] Only TailwindCSS classes used (no custom CSS)
+- [ ] Only TypeScript files (no .js files)
+- [ ] Procedural style (no classes, no OOP)
+- [ ] SSR for initial data (no AJAX on load)
+- [ ] HTMX for CRUD operations
+- [ ] Handler types used (no template.*Data duplicates)
+- [ ] Progressive enhancement works without JS
+
+**Documentation**:
+- [ ] User docs updated in `docs/user/dashboard.md`
+- [ ] API docs updated in `docs/developer/api/dashboard/`
+- [ ] Docs render at `/docs` endpoint
+- [ ] Search finds new content
+
+**Verification**:
+- [ ] `bash scripts/verify-guidelines.sh` passes (0 errors)
+- [ ] Git diff shows only intended changes
+- [ ] No secrets committed
+
+---
+
+## 🔗 Related Guidelines Compliance
+
+This plan addresses PROJECT_GUIDELINES.md requirements:
+
+✅ **Full-stack task with user approval** - backend modifications allowed
+✅ **No migration files** - merge into existing schema.sql
+✅ **Service layer architecture** - all logic in services, reusable
+✅ **TypeScript only** - no JavaScript files
+✅ **TailwindCSS only** - no custom CSS
+✅ **Procedural style** - no OOP, classes, or this-capture
+✅ **SSR for initial data** - no AJAX on page load
+✅ **HTMX for updates** - library switching, settings
+✅ **Share handler types** - no duplicate type systems
+✅ **Bruno tests** - all new endpoints tested
+✅ **Documentation** - docs/user and docs/developer/api updated
+✅ **Backward compatibility** - mobile apps supported
+
+---
+
+**Created:** 2025-02-17
+**Updated:** 2025-02-17
+**Status:** Planning
+**Priority:** High
+**Estimated effort:** 23-30 hours
+**Architecture:** SSR-First with HTMX for updates
+**Compliance:** PROJECT_GUIDELINES.md ✅
diff --git a/bruno-yaml/admin/Get Admin Library.yml b/bruno-yaml/admin/Get Admin Library.yml
new file mode 100644
index 0000000..b13a946
--- /dev/null
+++ b/bruno-yaml/admin/Get Admin Library.yml
@@ -0,0 +1,30 @@
+info:
+ name: Get Admin Library
+ type: http
+ seq: 5
+http:
+ method: GET
+ url: '{{base_url}}/admin/library'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get Admin Library Page
+
+ Retrieves the admin library page for administrative access.
+
+ **Method:** GET
+
+ **Endpoint:** /admin/library
+
+ **Headers:**
+ - `Authorization` (string): Bearer token
+
+ **Response:**
+ - HTML content for the admin library page
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Forbidden
diff --git a/bruno-yaml/admin/Get Admin Profile.yml b/bruno-yaml/admin/Get Admin Profile.yml
new file mode 100644
index 0000000..f6195ca
--- /dev/null
+++ b/bruno-yaml/admin/Get Admin Profile.yml
@@ -0,0 +1,39 @@
+info:
+ name: Get Admin Profile
+ type: http
+ seq: 4
+http:
+ method: GET
+ url: '{{base_url}}/admin/profile'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get Admin Profile
+
+ Retrieves the admin profile information for administrative access.
+
+ **Method:** GET
+
+ **Endpoint:** /admin/profile
+
+ **Authentication:** Required (Bearer token)
+
+ **Response:**
+ - JSON object containing admin profile details
+ - `id` (string): Admin user ID
+ - `email` (string): Admin email
+ - `username` (string): Admin username
+ - `theme` (string): Theme preference
+ - `first_name` (string): First name
+ - `last_name` (string): Last name
+ - `is_admin` (boolean): Admin status
+ - `created_at` (string): Account creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Forbidden (non-admin users)
+ - 404: Profile not found
diff --git a/bruno-yaml/analytics/Get Device Usage.yml b/bruno-yaml/analytics/Get Device Usage.yml
new file mode 100644
index 0000000..4dd78a0
--- /dev/null
+++ b/bruno-yaml/analytics/Get Device Usage.yml
@@ -0,0 +1,47 @@
+info:
+ name: Get Device Usage
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{base_url}}/api/analytics/device-usage'
+ auth: inherit
+ body:
+ type: none
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Get usage statistics for all devices.
+
+ **Endpoint**: GET /api/analytics/device-usage
+ **Auth**: Required (Bearer token)
+
+ ## Response Fields
+
+ | Field | Type | Description |
+ |-------|------|-------------|
+ | devices | array | List of device usage statistics |
+ | devices[].id | string | Device ID |
+ | devices[].device_name | string | Device name |
+ | devices[].device_type | string | Device type (kobo, kindle, koreader) |
+ | devices[].sync_count | int | Number of sync operations |
+ | devices[].last_sync | string | Last sync timestamp |
+ | devices[].total_reading_minutes | int | Total reading time on device |
+ | devices[].books_read | int | Number of books completed on device |
+
+ ## Example Response
+
+ ```json
+ {
+ "devices": [
+ {
+ "id": "789e4567-e89b-12d3-a456-426614174001",
+ "device_name": "My Kobo Clara",
+ "device_type": "kobo",
+ "sync_count": 45,
+ "last_sync": "2026-02-08T17:25:00Z",
+ "total_reading_minutes": 1250,
+ "books_read": 3
diff --git a/bruno-yaml/analytics/Get Popular Books.yml b/bruno-yaml/analytics/Get Popular Books.yml
new file mode 100644
index 0000000..21ecf50
--- /dev/null
+++ b/bruno-yaml/analytics/Get Popular Books.yml
@@ -0,0 +1,57 @@
+info:
+ name: Get Popular Books
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/analytics/popular-books?limit=10'
+ auth: inherit
+ body:
+ type: none
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Get popular books sorted by read count.
+
+ **Endpoint**: GET /api/analytics/popular-books
+ **Auth**: Required (Bearer token)
+
+ ## Query Parameters
+
+ | Parameter | Type | Required | Description |
+ |-----------|------|-----------|-------------|
+ | limit | int | No | Maximum number of books to return (default: 10) |
+
+ ## Response Fields
+
+ | Field | Type | Description |
+ |-------|------|-------------|
+ | books | array | List of popular books |
+ | books[].media_item_id | string | Book ID |
+ | books[].title | string | Book title |
+ | books[].author | string | Book author |
+ | books[].read_count | int | Number of times read |
+ | books[].avg_completion | float | Average completion rate (0-1) |
+ | books[].cover_url | string | Cover image URL |
+
+ ## Example Request
+
+ ```
+ GET /api/analytics/popular-books?limit=10
+ ```
+
+ ## Example Response
+
+ ```json
+ {
+ "books": [
+ {
+ "media_item_id": "323e4567-e89b-12d3-a456-426614174002",
+ "title": "The Great Gatsby",
+ "author": "F. Scott Fitzgerald",
+ "read_count": 5,
+ "avg_completion": 0.85,
+ "cover_url": "/api/books/323e4567-e89b-12d3-a456-426614174002/cover"
diff --git a/bruno-yaml/analytics/Get Reading Stats Date Range.yml b/bruno-yaml/analytics/Get Reading Stats Date Range.yml
new file mode 100644
index 0000000..14d4ba0
--- /dev/null
+++ b/bruno-yaml/analytics/Get Reading Stats Date Range.yml
@@ -0,0 +1,58 @@
+info:
+ name: Get Reading Stats Date Range
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31'
+ auth: inherit
+ body:
+ type: none
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Get reading statistics for a specific date range.
+
+ **Endpoint**: GET /api/analytics/reading-stats
+ **Auth**: Required (Bearer token)
+
+ ## Query Parameters
+
+ | Parameter | Type | Required | Description |
+ |-----------|------|-----------|-------------|
+ | start_date | string | No | Start date (ISO 8601 format, default: 30 days ago) |
+ | end_date | string | No | End date (ISO 8601 format, default: today) |
+
+ ## Response Fields
+
+ | Field | Type | Description |
+ |-------|------|-------------|
+ | total_books_read | int | Total books completed in range |
+ | total_pages_read | int | Total pages read in range |
+ | total_reading_time_minutes | int | Total reading time in minutes |
+ | completion_rate | float | Percentage of books completed (0-1) |
+ | daily_reading_minutes | array | Daily reading time per day |
+ | daily_reading_minutes[].date | string | Date (ISO 8601) |
+ | daily_reading_minutes[].minutes | int | Minutes read on that date |
+
+ ## Example Request
+
+ ```
+ GET /api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31
+ ```
+
+ ## Example Response
+
+ ```json
+ {
+ "total_books_read": 2,
+ "total_pages_read": 450,
+ "total_reading_time_minutes": 720,
+ "completion_rate": 0.85,
+ "daily_reading_minutes": [
+ {
+ "date": "2024-01-01",
+ "minutes": 30
diff --git a/bruno-yaml/analytics/Get Reading Stats.yml b/bruno-yaml/analytics/Get Reading Stats.yml
new file mode 100644
index 0000000..639d10d
--- /dev/null
+++ b/bruno-yaml/analytics/Get Reading Stats.yml
@@ -0,0 +1,58 @@
+info:
+ name: Get Reading Stats
+ type: http
+ seq: 4
+http:
+ method: GET
+ url: '{{base_url}}/api/analytics/reading-stats'
+ auth: inherit
+ body:
+ type: none
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Get overall reading statistics for the authenticated user.
+
+ **Endpoint**: GET /api/analytics/reading-stats
+ **Auth**: Required (Bearer token)
+
+ ## Query Parameters
+
+ | Parameter | Type | Required | Description |
+ |-----------|------|-----------|-------------|
+ | start_date | string | No | Start date (ISO 8601 format) |
+ | end_date | string | No | End date (ISO 8601 format) |
+
+ ## Response Fields
+
+ | Field | Type | Description |
+ |-------|------|-------------|
+ | total_books_read | int | Total books completed |
+ | total_pages_read | int | Total pages read |
+ | total_reading_time_minutes | int | Total reading time in minutes |
+ | completion_rate | float | Average book completion rate (0-1) |
+ | daily_reading_minutes | array | Daily reading time breakdown |
+ | daily_reading_minutes[].date | string | Date (ISO 8601) |
+ | daily_reading_minutes[].minutes | int | Minutes read on that date |
+
+ ## Example Request
+
+ ```
+ GET /api/analytics/reading-stats
+ ```
+
+ ## Example Response
+
+ ```json
+ {
+ "total_books_read": 12,
+ "total_pages_read": 3450,
+ "total_reading_time_minutes": 5400,
+ "completion_rate": 0.78,
+ "daily_reading_minutes": [
+ {
+ "date": "2026-01-15",
+ "minutes": 45
diff --git a/bruno-yaml/books/Bulk Delete Books.yml b/bruno-yaml/books/Bulk Delete Books.yml
new file mode 100644
index 0000000..447cb6e
--- /dev/null
+++ b/bruno-yaml/books/Bulk Delete Books.yml
@@ -0,0 +1,50 @@
+info:
+ name: Bulk Delete Media Items
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/media-items/bulk-delete'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"media_item_ids\": [\n \"{{bookId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Bulk Delete Media Items
+
+ Deletes multiple media items in a single request.
+
+ **Method:** POST
+
+ **Endpoint:** /api/media-items/bulk-delete
+
+ **Authentication:** Required (Bearer token)
+
+ **Request Body:**
+ - `media_item_ids` (array of strings): Array of media item UUIDs to delete
+
+ **Response:**
+ - `results` (array): Results for each deletion attempt
+ - `total` (number): Total number of media items processed
+ - `deleted` (number): Number of successfully deleted media items
+ - `failed` (number): Number of failed deletions
+
+ **Status Codes:**
+ - 200: Success (with partial results if some failed)
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 500: Internal server error
+
+ **Example:**
+ ```json
+ {
+ "media_item_ids": [
+ "uuid-1",
+ "uuid-2",
+ "uuid-3"
+ ]
diff --git a/bruno-yaml/books/Bulk Update Books.yml b/bruno-yaml/books/Bulk Update Books.yml
new file mode 100644
index 0000000..e7f5307
--- /dev/null
+++ b/bruno-yaml/books/Bulk Update Books.yml
@@ -0,0 +1,55 @@
+info:
+ name: Bulk Update Media Items
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/media-items/bulk-update'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"media_item_updates\": [\n {\n \"media_item_id\"\
+ : \"{{bookId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Bulk Update Media Items
+
+ Updates multiple media items in a single request with different fields for each item.
+
+ **Method:** POST
+
+ **Endpoint:** /api/media-items/bulk-update
+
+ **Authentication:** Required (Bearer token)
+
+ **Request Body:**
+ - `media_item_updates` (array): Array of update objects
+ - `media_item_id` (string): Media item UUID to update
+ - `updates` (object): Fields to update (can include title, author, genre, tags, contributors, etc.)
+
+ **Response:**
+ - `results` (array): Results for each update attempt
+ - `total` (number): Total number of media items processed
+ - `updated` (number): Number of successfully updated media items
+ - `failed` (number): Number of failed updates
+
+ **Status Codes:**
+ - 200: Success (with partial results if some failed)
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 500: Internal server error
+
+ **Example:**
+ ```json
+ {
+ "media_item_updates": [
+ {
+ "media_item_id": "uuid-1",
+ "updates": {
+ "title": "New Title",
+ "genre": "Fiction",
+ "tags": ["fiction", "adventure"]
diff --git a/bruno-yaml/collections/Add Books to Collection.yml b/bruno-yaml/collections/Add Books to Collection.yml
new file mode 100644
index 0000000..04a4974
--- /dev/null
+++ b/bruno-yaml/collections/Add Books to Collection.yml
@@ -0,0 +1,11 @@
+info:
+ name: Add Books to Collection
+ type: http
+ seq: 6
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/{{collection_id}}/books'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"book_ids\": [\n \"{{book_id_1"
diff --git a/bruno-yaml/collections/Create Collection.yml b/bruno-yaml/collections/Create Collection.yml
new file mode 100644
index 0000000..38588ab
--- /dev/null
+++ b/bruno-yaml/collections/Create Collection.yml
@@ -0,0 +1,15 @@
+info:
+ name: Create Collection
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/collections'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"name\": \"Science Fiction\",\n \"description\": \"My favorite\
+ \ sci-fi books\",\n \"color\": \"#ff0000\",\n \"icon\": \"\U0001F680\"\
+ ,\n \"auto_assign_rules\": [\n {\n \"id\": \"rule-1\",\n \
+ \ \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\"\
+ : \"Science Fiction\",\n \"priority\": 8"
diff --git a/bruno-yaml/collections/Create Device Mapping.yml b/bruno-yaml/collections/Create Device Mapping.yml
new file mode 100644
index 0000000..c40a2c7
--- /dev/null
+++ b/bruno-yaml/collections/Create Device Mapping.yml
@@ -0,0 +1,11 @@
+info:
+ name: Create Device Mapping
+ type: http
+ seq: 9
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/{{device_id}}/collections'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"collection_id\": \"{{collection_id"
diff --git a/bruno-yaml/collections/Delete Collection.yml b/bruno-yaml/collections/Delete Collection.yml
new file mode 100644
index 0000000..381a873
--- /dev/null
+++ b/bruno-yaml/collections/Delete Collection.yml
@@ -0,0 +1,8 @@
+info:
+ name: Delete Collection
+ type: http
+ seq: 5
+http:
+ method: DELETE
+ url: '{{base_url}}/api/collections/{{collection_id}}'
+ auth: inherit
diff --git a/bruno-yaml/collections/Delete Device Mapping.yml b/bruno-yaml/collections/Delete Device Mapping.yml
new file mode 100644
index 0000000..4469fb9
--- /dev/null
+++ b/bruno-yaml/collections/Delete Device Mapping.yml
@@ -0,0 +1,8 @@
+info:
+ name: Delete Device Mapping
+ type: http
+ seq: 11
+http:
+ method: DELETE
+ url: '{{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}}'
+ auth: inherit
diff --git a/bruno-yaml/collections/Get Book Collections.yml b/bruno-yaml/collections/Get Book Collections.yml
new file mode 100644
index 0000000..c8fc6dd
--- /dev/null
+++ b/bruno-yaml/collections/Get Book Collections.yml
@@ -0,0 +1,8 @@
+info:
+ name: Get Book Collections
+ type: http
+ seq: 12
+http:
+ method: GET
+ url: '{{base_url}}/api/collections/books/{{book_id}}'
+ auth: inherit
diff --git a/bruno-yaml/collections/Get Collection.yml b/bruno-yaml/collections/Get Collection.yml
new file mode 100644
index 0000000..aac4e33
--- /dev/null
+++ b/bruno-yaml/collections/Get Collection.yml
@@ -0,0 +1,8 @@
+info:
+ name: Get Collection
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/collections/{{collection_id}}'
+ auth: inherit
diff --git a/bruno-yaml/collections/Get Collections.yml b/bruno-yaml/collections/Get Collections.yml
new file mode 100644
index 0000000..d409e09
--- /dev/null
+++ b/bruno-yaml/collections/Get Collections.yml
@@ -0,0 +1,8 @@
+info:
+ name: Get Collections
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{base_url}}/api/collections?include_auto=true&sort_by=name'
+ auth: inherit
diff --git a/bruno-yaml/collections/Get Device Mappings.yml b/bruno-yaml/collections/Get Device Mappings.yml
new file mode 100644
index 0000000..7b60cca
--- /dev/null
+++ b/bruno-yaml/collections/Get Device Mappings.yml
@@ -0,0 +1,8 @@
+info:
+ name: Get Device Mappings
+ type: http
+ seq: 8
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/{{device_id}}/collections'
+ auth: inherit
diff --git a/bruno-yaml/collections/Remove Book from Collection.yml b/bruno-yaml/collections/Remove Book from Collection.yml
new file mode 100644
index 0000000..d7e7e98
--- /dev/null
+++ b/bruno-yaml/collections/Remove Book from Collection.yml
@@ -0,0 +1,8 @@
+info:
+ name: Remove Book from Collection
+ type: http
+ seq: 7
+http:
+ method: DELETE
+ url: '{{base_url}}/api/collections/{{collection_id}}/books/{{book_id}}'
+ auth: inherit
diff --git a/bruno-yaml/collections/Update Collection.yml b/bruno-yaml/collections/Update Collection.yml
new file mode 100644
index 0000000..9a7ae3e
--- /dev/null
+++ b/bruno-yaml/collections/Update Collection.yml
@@ -0,0 +1,15 @@
+info:
+ name: Update Collection
+ type: http
+ seq: 4
+http:
+ method: PUT
+ url: '{{base_url}}/api/collections/{{collection_id}}'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"name\": \"Sci-Fi Favorites\",\n \"description\": \"Updated\
+ \ description\",\n \"color\": \"#00ff00\",\n \"icon\": \"⭐\",\n \"\
+ auto_assign_rules\": [\n {\n \"id\": \"rule-2\",\n \"field\"\
+ : \"series\",\n \"operator\": \"equals\",\n \"value\": \"Foundation\"\
+ ,\n \"priority\": 9"
diff --git a/bruno-yaml/collections/Update Device Mapping.yml b/bruno-yaml/collections/Update Device Mapping.yml
new file mode 100644
index 0000000..63b834b
--- /dev/null
+++ b/bruno-yaml/collections/Update Device Mapping.yml
@@ -0,0 +1,12 @@
+info:
+ name: Update Device Mapping
+ type: http
+ seq: 10
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}}'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"device_shelf_name\": \"Science Fiction\",\n \"sync_direction\"\
+ : \"book_to_device\""
diff --git a/bruno-yaml/collections/scenarios/Bulk Add Books to Collections.yml b/bruno-yaml/collections/scenarios/Bulk Add Books to Collections.yml
new file mode 100644
index 0000000..365991f
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Bulk Add Books to Collections.yml
@@ -0,0 +1,53 @@
+info:
+ name: Bulk Add Books to Collections
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/bulk-add-books'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"operations\": [\n {\n \"collection_id\": \"{{collectionId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Bulk Add Books to Collections
+
+ Adds multiple books to multiple collections in a single request. Each operation specifies a collection and a list of books to add.
+
+ **Method:** POST
+
+ **Endpoint:** /api/collections/bulk-add-books
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `operations` (array): Array of collection-book operations
+ - `collection_id` (string): Collection UUID
+ - `book_ids` (array): Array of book UUIDs to add to the collection
+
+ **Response:**
+ - `results` (array): Results for each operation
+ - `total` (number): Total number of operations
+ - `success` (number): Number of successful operations
+ - `failed` (number): Number of failed operations
+
+ **Status Codes:**
+ - 200: Success (with partial results if some failed)
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 500: Internal server error
+
+ **Example:**
+ ```json
+ {
+ "operations": [
+ {
+ "collection_id": "collection-uuid-1",
+ "book_ids": ["book-1", "book-2"]
diff --git a/bruno-yaml/collections/scenarios/Bulk Remove Books - All Books.yml b/bruno-yaml/collections/scenarios/Bulk Remove Books - All Books.yml
new file mode 100644
index 0000000..e8826d8
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Bulk Remove Books - All Books.yml
@@ -0,0 +1,23 @@
+info:
+ name: Bulk Remove Books - All Books
+ type: http
+ seq: 5
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"book_ids\": [\n \"{{bookId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Bulk Remove Books from Collection
+
+ Removes multiple books from a collection in a single request.
+
+ **Method:** POST
+
+ **Endpoint:** /api/collections/{collection_id
diff --git a/bruno-yaml/collections/scenarios/Bulk Remove Books - Empty List.yml b/bruno-yaml/collections/scenarios/Bulk Remove Books - Empty List.yml
new file mode 100644
index 0000000..f0638e3
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Bulk Remove Books - Empty List.yml
@@ -0,0 +1,27 @@
+info:
+ name: Bulk Remove Books - Empty List
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"book_ids\": []"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Bulk Remove Books - Empty List Validation
+
+ Tests validation behavior when providing an empty book_ids array.
+
+ **Expected Result:** 400 Bad Request
+
+ **Validation Rule:** book_ids array must contain at least one book UUID.
+
+ **Purpose:** Ensures the API properly validates input and rejects empty removal requests.
diff --git a/bruno-yaml/collections/scenarios/Bulk Remove Books - Invalid IDs.yml b/bruno-yaml/collections/scenarios/Bulk Remove Books - Invalid IDs.yml
new file mode 100644
index 0000000..4822991
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Bulk Remove Books - Invalid IDs.yml
@@ -0,0 +1,31 @@
+info:
+ name: Bulk Remove Books - Invalid IDs
+ type: http
+ seq: 4
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"book_ids\": [\n \"{{bookId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Bulk Remove Books - Invalid IDs
+
+ Tests behavior when the book_ids array contains invalid UUID formats or non-existent books.
+
+ **Expected Result:** 200 OK with partial success
+
+ **Purpose:** Verifies that:
+ - Invalid UUID formats don't crash the endpoint
+ - Non-existent book IDs are handled gracefully
+ - Valid IDs in the same request are still processed
+ - Response includes detailed results showing which succeeded/failed
+
+ **Note:** The endpoint should process all valid IDs and report failures for invalid ones, allowing clients to handle partial failures appropriately.
diff --git a/bruno-yaml/collections/scenarios/Bulk Remove Books - Single Book.yml b/bruno-yaml/collections/scenarios/Bulk Remove Books - Single Book.yml
new file mode 100644
index 0000000..1fb1df9
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Bulk Remove Books - Single Book.yml
@@ -0,0 +1,27 @@
+info:
+ name: Bulk Remove Books - Single Book
+ type: http
+ seq: 3
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"book_ids\": [\n \"{{bookId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Bulk Remove Books - Single Book
+
+ Tests that bulk remove endpoint works correctly with a single book.
+
+ **Expected Result:** 200 OK with removed: 1, total: 1
+
+ **Purpose:** Verifies the bulk remove endpoint handles single-item arrays correctly, providing flexibility for clients to use the same endpoint for both single and multiple removals.
+
+ **Note:** Using bulk remove for a single book is functionally equivalent to the single remove endpoint but allows for consistent error handling and response format.
diff --git a/bruno-yaml/collections/scenarios/Test Collection Rules - Author Contains.yml b/bruno-yaml/collections/scenarios/Test Collection Rules - Author Contains.yml
new file mode 100644
index 0000000..3d1b3ed
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Test Collection Rules - Author Contains.yml
@@ -0,0 +1,30 @@
+info:
+ name: Test Collection Rules - Author Contains
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/test-rules'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"rules\": [\n {\n \"field\": \"author\",\n \
+ \ \"operator\": \"contains\",\n \"value\": \"Asimov\""
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Test Collection Rules - Author Contains
+
+ Tests the "contains" operator on the author field to find books by a specific author (partial match).
+
+ **Example Use Case:** Finding all books by an author whose name contains "Asimov" (e.g., "Isaac Asimov").
+
+ **Operator:** `contains` - Matches if the field contains the specified value as a substring (case-insensitive typically).
+
+ **Expected Result:** Returns all books where the author field contains "Asimov".
+
+ **Purpose:** Demonstrates text-based partial matching for author searches, useful when you don't need the exact author name or want to find books by authors with similar names.
diff --git a/bruno-yaml/collections/scenarios/Test Collection Rules - Copyright Year Greater Than.yml b/bruno-yaml/collections/scenarios/Test Collection Rules - Copyright Year Greater Than.yml
new file mode 100644
index 0000000..e61e353
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Test Collection Rules - Copyright Year Greater Than.yml
@@ -0,0 +1,32 @@
+info:
+ name: Test Collection Rules - Copyright Year Greater Than
+ type: http
+ seq: 3
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/test-rules'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"rules\": [\n {\n \"field\": \"copyright_year\"\
+ ,\n \"operator\": \"greater_than\",\n \"value\": \"2000\""
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Test Collection Rules - Copyright Year Greater Than
+
+ Tests the "greater_than" operator on the copyright_year field to find books published after a specific year.
+
+ **Example Use Case:** Creating a "Modern Books" collection with books published after 2000.
+
+ **Operator:** `greater_than` - Matches if the field value is greater than the specified value (numeric comparison).
+
+ **Field:** `copyright_year` - The year the book was copyrighted/published.
+
+ **Expected Result:** Returns all books with copyright_year greater than 2000 (i.e., published in 2001 or later).
+
+ **Purpose:** Demonstrates numeric comparison operators for creating date-based collections, useful for organizing books by publication era.
diff --git a/bruno-yaml/collections/scenarios/Test Collection Rules - Empty Rules Array.yml b/bruno-yaml/collections/scenarios/Test Collection Rules - Empty Rules Array.yml
new file mode 100644
index 0000000..eb37ac0
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Test Collection Rules - Empty Rules Array.yml
@@ -0,0 +1,29 @@
+info:
+ name: Test Collection Rules - Empty Rules Array
+ type: http
+ seq: 5
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/test-rules'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"rules\": []"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Test Collection Rules - Empty Rules Array
+
+ Tests validation behavior when providing an empty rules array.
+
+ **Expected Result:** 400 Bad Request
+
+ **Validation Rule:** rules array must contain at least one rule object.
+
+ **Purpose:** Ensures the API properly validates input and rejects empty rule sets, preventing accidental queries that would return all books or cause performance issues.
+
+ **Use Case:** Client-side validation should prevent sending empty rules, but the API should also validate to catch malformed requests.
diff --git a/bruno-yaml/collections/scenarios/Test Collection Rules - No Matches.yml b/bruno-yaml/collections/scenarios/Test Collection Rules - No Matches.yml
new file mode 100644
index 0000000..79eeb42
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Test Collection Rules - No Matches.yml
@@ -0,0 +1,34 @@
+info:
+ name: Test Collection Rules - No Matches
+ type: http
+ seq: 4
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/test-rules'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \
+ \ \"operator\": \"equals\",\n \"value\": \"NonExistentGenre123456\""
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Test Collection Rules - No Matches
+
+ Tests behavior when collection rules don't match any books in the library.
+
+ **Example Use Case:** Validating that a new genre name doesn't exist before creating a collection for it, or testing edge cases.
+
+ **Expected Result:** 200 OK with empty matches array and total: 0
+
+ **Purpose:** Verifies that the API handles zero-match scenarios gracefully:
+ - Returns 200 (success) not 404
+ - Returns empty array, not null
+ - Returns total: 0 for clarity
+ - No errors thrown for no results
+
+ **Note:** An empty result set is a valid response and doesn't indicate an error. This allows users to test rules confidently before creating collections.
diff --git a/bruno-yaml/collections/scenarios/Test Collection Rules.yml b/bruno-yaml/collections/scenarios/Test Collection Rules.yml
new file mode 100644
index 0000000..752d0ae
--- /dev/null
+++ b/bruno-yaml/collections/scenarios/Test Collection Rules.yml
@@ -0,0 +1,67 @@
+info:
+ name: Test Collection Rules
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/collections/test-rules'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \
+ \ \"operator\": \"equals\",\n \"value\": \"Science Fiction\""
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Test Collection Rules
+
+ Tests collection rules against the library to see which books match, without creating a collection. Useful for previewing what books would be included in a collection with specific rules.
+
+ **Method:** POST
+
+ **Endpoint:** /api/collections/test-rules
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `rules` (array): Array of rule objects to test
+ - `field` (string): Field to test (genre, author, copyright_year, tags, etc.)
+ - `operator` (string): Comparison operator
+ - `equals`: Exact match
+ - `contains`: Contains substring (for text fields)
+ - `greater_than`: Greater than (for numeric fields)
+ - `less_than`: Less than (for numeric fields)
+ - `not_equals`: Not equal to
+ - `starts_with`: Starts with
+ - `ends_with`: Ends with
+ - `is_empty`: Field is empty or null
+ - `is_not_empty`: Field is not empty and not null
+ - `value` (string): Value to compare against (not required for is_empty/is_not_empty)
+
+ **Response:**
+ - `matches` (array): Array of matching books
+ - `id` (string): Book UUID
+ - `title` (string): Book title
+ - `author` (string): Book author
+ - `genre` (string): Book genre
+ - Additional book metadata
+ - `total` (number): Total number of matching books
+
+ **Status Codes:**
+ - 200: Success - returns matching books
+ - 400: Invalid request (empty rules array, invalid field/operator)
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Example Request:**
+ ```json
+ {
+ "rules": [
+ {
+ "field": "genre",
+ "operator": "equals",
+ "value": "Science Fiction"
diff --git a/bruno-yaml/conflicts/Delete Conflict.yml b/bruno-yaml/conflicts/Delete Conflict.yml
new file mode 100644
index 0000000..de05a3e
--- /dev/null
+++ b/bruno-yaml/conflicts/Delete Conflict.yml
@@ -0,0 +1,22 @@
+info:
+ name: Delete Conflict
+ type: http
+ seq: 4
+http:
+ method: DELETE
+ url: '{{base_url}}/api/conflicts/{{conflict_id}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{token
+
+docs: |-
+ ## Delete Conflict
+
+ Permanently deletes a specific conflict record from the system.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/conflicts/{conflict_id
diff --git a/bruno-yaml/conflicts/Dismiss All Resolved.yml b/bruno-yaml/conflicts/Dismiss All Resolved.yml
new file mode 100644
index 0000000..5227bdb
--- /dev/null
+++ b/bruno-yaml/conflicts/Dismiss All Resolved.yml
@@ -0,0 +1,37 @@
+info:
+ name: Dismiss All Resolved Conflicts
+ type: http
+ seq: 5
+http:
+ method: POST
+ url: '{{base_url}}/api/conflicts/dismiss-all'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{token
+
+docs: |-
+ ## Dismiss All Resolved Conflicts
+
+ Deletes all resolved conflicts for the authenticated user, cleaning up the conflict list.
+
+ **Method:** POST
+
+ **Endpoint:** /api/conflicts/dismiss-all
+
+ **Authentication:** Bearer token
+
+ **Response:**
+ - `deleted` (number): Number of conflict records that were deleted
+
+ **Status Codes:**
+ - 200: Success - conflicts deleted
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Example Response:**
+ ```json
+ {
+ "deleted": 5
diff --git a/bruno-yaml/conflicts/Get Conflict Details.yml b/bruno-yaml/conflicts/Get Conflict Details.yml
new file mode 100644
index 0000000..d3f81af
--- /dev/null
+++ b/bruno-yaml/conflicts/Get Conflict Details.yml
@@ -0,0 +1,22 @@
+info:
+ name: Get Conflict Details
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{base_url}}/api/conflicts/{{conflict_id}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{token
+
+docs: |-
+ ## Get Conflict Details
+
+ Retrieves detailed information about a specific conflict, including side-by-side comparison of conflicting data from all sources.
+
+ **Method:** GET
+
+ **Endpoint:** /api/conflicts/{conflict_id
diff --git a/bruno-yaml/conflicts/List Conflicts.yml b/bruno-yaml/conflicts/List Conflicts.yml
new file mode 100644
index 0000000..dfe720b
--- /dev/null
+++ b/bruno-yaml/conflicts/List Conflicts.yml
@@ -0,0 +1,74 @@
+info:
+ name: List Conflicts
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/conflicts?status=unresolved'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{token
+
+docs: |-
+ ## List Conflicts
+
+ Lists all sync conflicts for the authenticated user with optional filtering.
+
+ **Method:** GET
+
+ **Endpoint:** /api/conflicts
+
+ **Authentication:** Bearer token
+
+ **Query Parameters:**
+ - `status` (string, optional): Filter by resolution status
+ - `unresolved`: Only unresolved conflicts (default)
+ - `user_resolved`: Conflicts resolved by user
+ - `auto_resolved`: Automatically resolved conflicts
+ - `all`: All conflicts regardless of status
+ - `type` (string, optional): Filter by conflict type
+ - `progress`: Reading progress conflicts
+ - `note`: Bookmark/note conflicts
+ - `highlight`: Highlight conflicts
+
+ **Response:**
+ - `conflicts` (array): Array of conflict objects
+ - `total` (number): Total number of conflicts matching filters
+ - `unresolved` (number): Number of unresolved conflicts
+
+ **Each Conflict Object:**
+ - `id` (string): Conflict UUID
+ - `media_item_id` (string): Associated book UUID
+ - `media_item_title` (string): Book title
+ - `conflict_type` (string): Type of conflict (progress, note, highlight)
+ - `conflict_data` (object): Side-by-side comparison of conflicting data
+ - `koreader`: Data from KOReader device
+ - `kobo`: Data from Kobo device
+ - `web`: Data from web interface
+ - `resolution_status` (string): Current status (unresolved, user_resolved, auto_resolved)
+ - `created_at` (string): ISO 8601 timestamp when conflict was detected
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Example Request:**
+ ```
+ GET /api/conflicts?status=unresolved&type=progress
+ ```
+
+ **Example Response:**
+ ```json
+ {
+ "conflicts": [
+ {
+ "id": "conflict-uuid",
+ "media_item_id": "book-uuid",
+ "media_item_title": "Foundation",
+ "conflict_type": "progress",
+ "conflict_data": {
+ "koreader": { "percentage": 0.65, "epubcfi": "..."
diff --git a/bruno-yaml/conflicts/Resolve Conflict.yml b/bruno-yaml/conflicts/Resolve Conflict.yml
new file mode 100644
index 0000000..d022616
--- /dev/null
+++ b/bruno-yaml/conflicts/Resolve Conflict.yml
@@ -0,0 +1,25 @@
+info:
+ name: Resolve Conflict
+ type: http
+ seq: 3
+http:
+ method: POST
+ url: '{{base_url}}/api/conflicts/{{conflict_id}}/resolve'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"winner\": \"koreader\",\n \"manual_data\": null,\n \"\
+ apply_to_all_future_conflicts\": false,\n \"reason\": \"User chose more recent\
+ \ progress\""
+ headers:
+ - key: Authorization
+ value: Bearer {{token
+
+docs: |-
+ ## Resolve Conflict
+
+ Resolves a sync conflict by choosing which source to use for the conflicting data.
+
+ **Method:** POST
+
+ **Endpoint:** /api/conflicts/{conflict_id
diff --git a/bruno-yaml/conflicts/scenarios/Bulk Dismiss Conflicts.yml b/bruno-yaml/conflicts/scenarios/Bulk Dismiss Conflicts.yml
new file mode 100644
index 0000000..47269ec
--- /dev/null
+++ b/bruno-yaml/conflicts/scenarios/Bulk Dismiss Conflicts.yml
@@ -0,0 +1,49 @@
+info:
+ name: Bulk Dismiss Conflicts
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/conflicts/bulk-dismiss'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Bulk Dismiss Conflicts
+
+ Dismisses multiple sync conflicts without resolving them. This removes them from the conflict list while leaving the data unchanged.
+
+ **Method:** POST
+
+ **Endpoint:** /api/conflicts/bulk-dismiss
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `conflict_ids` (array): Array of conflict UUIDs to dismiss
+
+ **Response:**
+ - `results` (array): Results for each dismissal
+ - `total` (number): Total number of conflicts processed
+ - `success` (number): Number of successfully dismissed conflicts
+ - `failed` (number): Number of failed dismissals
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 404: One or more conflicts not found
+ - 500: Internal server error
+
+ **Example:**
+ ```json
+ {
+ "conflict_ids": ["uuid-1", "uuid-2"]
diff --git a/bruno-yaml/conflicts/scenarios/Bulk Resolve Conflicts.yml b/bruno-yaml/conflicts/scenarios/Bulk Resolve Conflicts.yml
new file mode 100644
index 0000000..433505e
--- /dev/null
+++ b/bruno-yaml/conflicts/scenarios/Bulk Resolve Conflicts.yml
@@ -0,0 +1,55 @@
+info:
+ name: Bulk Resolve Conflicts
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/conflicts/bulk-resolve'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Bulk Resolve Conflicts
+
+ Resolves multiple sync conflicts in a single request using a specified resolution strategy.
+
+ **Method:** POST
+
+ **Endpoint:** /api/conflicts/bulk-resolve
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `conflict_ids` (array): Array of conflict UUIDs to resolve
+ - `strategy` (string): Resolution strategy
+ - `most_recent`: Use the most recently updated progress
+ - `highest_progress`: Use the reading progress with the highest percent read
+ - `server`: Always prefer server-side data
+ - `device`: Always prefer device-side data
+
+ **Response:**
+ - `results` (array): Results for each conflict resolution
+ - `total` (number): Total number of conflicts processed
+ - `success` (number): Number of successfully resolved conflicts
+ - `failed` (number): Number of failed resolutions
+
+ **Status Codes:**
+ - 200: Success (with partial results if some failed)
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 404: One or more conflicts not found
+ - 500: Internal server error
+
+ **Example:**
+ ```json
+ {
+ "conflict_ids": ["uuid-1", "uuid-2", "uuid-3"],
+ "strategy": "most_recent"
diff --git a/bruno-yaml/conflicts/scenarios/Bulk Resolve Highest Progress.yml b/bruno-yaml/conflicts/scenarios/Bulk Resolve Highest Progress.yml
new file mode 100644
index 0000000..aff45fb
--- /dev/null
+++ b/bruno-yaml/conflicts/scenarios/Bulk Resolve Highest Progress.yml
@@ -0,0 +1,46 @@
+info:
+ name: Bulk Resolve with Highest Progress Strategy
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/conflicts/bulk-resolve'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Bulk Resolve with Highest Progress Strategy
+
+ Resolves multiple sync conflicts using the "highest_progress" strategy, which keeps the reading progress with the highest percentage read.
+
+ **Method:** POST
+
+ **Endpoint:** /api/conflicts/bulk-resolve
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `conflict_ids` (array): Array of conflict UUIDs to resolve
+ - `strategy` (string): Must be "highest_progress"
+
+ **Response:**
+ - `results` (array): Results for each conflict resolution
+ - `total` (number): Total number of conflicts processed
+ - `success` (number): Number of successfully resolved conflicts
+ - `failed` (number): Number of failed resolutions
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 500: Internal server error
+
+ **Note:** The highest progress strategy is ideal when you want to preserve the most reading progress across devices. Use this when you've been reading on multiple devices and want to keep the furthest position.
diff --git a/bruno-yaml/dashboard/create-collection-with-dashboard.yml b/bruno-yaml/dashboard/create-collection-with-dashboard.yml
new file mode 100644
index 0000000..eeeaf7a
--- /dev/null
+++ b/bruno-yaml/dashboard/create-collection-with-dashboard.yml
@@ -0,0 +1,38 @@
+info:
+ name: Create Collection with Dashboard
+ type: http
+ seq: 4
+http:
+ method: POST
+ url: '{{base_url}}/api/collections'
+ auth: inherit
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"creates collection successfully\", function() {\n expect(res.status).to.eql(201);"
+
+docs: |-
+ Create a new collection with dashboard visibility enabled.
+
+ **Endpoint**: POST /api/collections
+ **Auth**: Required (Bearer token)
+
+ ## Request Body
+
+ | Field | Type | Required | Description |
+ |-------|------|----------|-------------|
+ | name | string | Yes | Collection name |
+ | description | string | No | Collection description |
+ | color | string | No | Hex color code |
+ | icon | string | No | Icon emoji or name |
+ | show_on_dashboard | boolean | No | Show on dashboard (default: false) |
+
+ ## Example Request
+
+ ```json
+ {
+ "name": "My Favorites",
+ "description": "My favorite books",
+ "color": "#FF5733",
+ "icon": "⭐",
+ "show_on_dashboard": true
diff --git a/bruno-yaml/dashboard/get-dashboard-sections.yml b/bruno-yaml/dashboard/get-dashboard-sections.yml
new file mode 100644
index 0000000..3f3382d
--- /dev/null
+++ b/bruno-yaml/dashboard/get-dashboard-sections.yml
@@ -0,0 +1,52 @@
+info:
+ name: Get Dashboard Sections
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/dashboard/sections'
+ auth: inherit
+ body:
+ type: none
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200 with auth\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Get all dashboard sections for a specific library.
+
+ **Endpoint**: GET /api/dashboard/sections
+ **Auth**: Required (Bearer token via auth: inherit)
+
+ ## Query Parameters
+
+ | Parameter | Type | Required | Description |
+ |-----------|------|----------|-------------|
+ | library_id | string | Yes | Library UUID |
+
+ ## Response
+
+ Returns array of sections including:
+ - Smart sections (continue-reading, in-progress, recently-added, etc.)
+ - User collections marked with show_on_dashboard: true
+
+ ## Section Types
+
+ | Type | Description |
+ |------|-------------|
+ | smart | Auto-generated sections based on user activity |
+ | collection | User-created collections with dashboard enabled |
+
+ ## Example Response
+
+ ```json
+ {
+ "sections": [
+ {
+ "id": "continue-reading",
+ "type": "smart",
+ "title": "Continue Reading",
+ "icon": "📖",
+ "items": [...],
+ "view_all_url": "/section/continue-reading"
diff --git a/bruno-yaml/dashboard/get-sections-by-library.yml b/bruno-yaml/dashboard/get-sections-by-library.yml
new file mode 100644
index 0000000..ad95cb8
--- /dev/null
+++ b/bruno-yaml/dashboard/get-sections-by-library.yml
@@ -0,0 +1,30 @@
+info:
+ name: Get Dashboard Sections by Library
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/dashboard/sections'
+ auth: inherit
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Get all dashboard sections for a specific library.
+
+ **Endpoint**: GET /api/dashboard/sections
+ **Auth**: Required (Bearer token)
+
+ ## Query Parameters
+
+ | Parameter | Type | Required | Description |
+ |-----------|------|----------|-------------|
+ | library_id | string | Yes | Library UUID |
+
+ ## Response
+
+ Returns array of sections including:
+ - Smart sections (continue-reading, in-progress, recently-added, etc.)
+ - User collections marked for dashboard
diff --git a/bruno-yaml/dashboard/update-collection-visibility.yml b/bruno-yaml/dashboard/update-collection-visibility.yml
new file mode 100644
index 0000000..3f131f0
--- /dev/null
+++ b/bruno-yaml/dashboard/update-collection-visibility.yml
@@ -0,0 +1,17 @@
+info:
+ name: Update Collection Dashboard Visibility
+ type: http
+ seq: 5
+http:
+ method: PUT
+ url: '{{base_url}}/api/collections/{{collection_id}}'
+ auth: inherit
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"updates collection successfully\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Update collection settings including dashboard visibility.
+
+ **Endpoint**: PUT /api/collections/{id
diff --git a/bruno-yaml/dashboard/update-preferences.yml b/bruno-yaml/dashboard/update-preferences.yml
new file mode 100644
index 0000000..d8913ca
--- /dev/null
+++ b/bruno-yaml/dashboard/update-preferences.yml
@@ -0,0 +1,36 @@
+info:
+ name: Update Dashboard Preferences
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/dashboard/preferences'
+ auth: inherit
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200 with valid request\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Update dashboard preferences for the authenticated user.
+
+ **Endpoint**: POST /api/dashboard/preferences
+ **Auth**: Required (Bearer token)
+
+ ## Request Body
+
+ | Field | Type | Required | Description |
+ |-------|------|----------|-------------|
+ | library_id | string | Yes | Library UUID |
+ | hidden_sections | array | No | Section IDs to hide |
+ | section_order | array | No | Section IDs in custom order |
+ | items_per_section | int | No | Items to show per section (10-50) |
+
+ ## Example Request
+
+ ```json
+ {
+ "library_id": "cc23c3a7-f8fb-451a-a78d-2a16df1b725a",
+ "hidden_sections": ["recently-added"],
+ "section_order": ["continue-reading", "in-progress"],
+ "items_per_section": 25
diff --git a/bruno-yaml/devices/api.yml b/bruno-yaml/devices/api.yml
new file mode 100644
index 0000000..e432bd9
--- /dev/null
+++ b/bruno-yaml/devices/api.yml
@@ -0,0 +1,7 @@
+info:
+ name: Bookhoard Device Management API
+ type: collection
+ seq: 1
+http:
+ method: POST
+ url: '"http://localhost:8765/api"'
diff --git a/bruno-yaml/devices/kobo/Get Analytics Tests.yml b/bruno-yaml/devices/kobo/Get Analytics Tests.yml
new file mode 100644
index 0000000..cc1a07b
--- /dev/null
+++ b/bruno-yaml/devices/kobo/Get Analytics Tests.yml
@@ -0,0 +1,96 @@
+info:
+ name: Get Analytics Tests
+ type: http
+ seq: 7
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/kobo/v1/analytics/gettests'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ platform: android
+ firmware_version: 4.30.19023
+
+docs: |-
+ ## Get Kobo Analytics Tests
+
+ Retrieves A/B testing configuration and feature flags for Kobo device.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/kobo/v1/analytics/gettests
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `platform` (string): Device platform
+ - `android`: Kobo Android app
+ - `kobo`: Native Kobo firmware
+ - `firmware_version` (string): Firmware version (e.g., "4.30.19023")
+ - `device_model` (string, optional): Device model identifier
+ - `locale` (string, optional): Device locale (e.g., "en_US")
+
+ **Response:**
+ - `tests` (array): Active A/B tests
+ - `test_name` (string): Test identifier
+ - `variant` (string): Assigned variant (A, B, C, etc.)
+ - `enabled` (boolean): Whether test is active
+ - `parameters` (object): Test-specific parameters
+ - `features` (object): Feature flags
+ - `feature_name` (boolean/string): Feature state
+ - `configuration` (object): Device configuration
+ - `sync_interval_minutes` (integer): Recommended sync frequency
+ - `batch_size` (integer): Max items per batch sync
+ - `timeout_seconds` (integer): Request timeout
+ - `version` (string): Configuration version
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 400: Invalid request parameters
+
+ **Analytics Testing Purpose:**
+ - Kobo uses A/B testing for UX features
+ - Feature flags for gradual rollout
+ - Performance monitoring configuration
+ - Sync behavior optimization
+ - Device-specific tuning
+
+ **Common Tests:**
+ - Sync frequency optimization
+ - UI/UX variations
+ - Network usage patterns
+ - Battery life improvements
+ - Feature set variations by model
+
+ **Feature Flags:**
+ - New sync features
+ - Beta functionality
+ - Platform-specific capabilities
+ - Experimental features
+
+ **Configuration Parameters:**
+ - Optimal sync intervals for device
+ - Batch size limits based on device capabilities
+ - Timeout values for network conditions
+ - Retry logic configuration
+ - Cache policy settings
+
+ **Usage:**
+ - Called during device initialization
+ - Refreshed daily or weekly
+ - Cached on device
+ - Affects sync behavior
+ - Can be overridden by server
+
+ **Use Cases:**
+ - Device initialization
+ - Feature rollout testing
+ - Performance optimization
+ - UX experiment participation
+ - Configuration management
diff --git a/bruno-yaml/devices/kobo/Get Kobo Library.yml b/bruno-yaml/devices/kobo/Get Kobo Library.yml
new file mode 100644
index 0000000..8851559
--- /dev/null
+++ b/bruno-yaml/devices/kobo/Get Kobo Library.yml
@@ -0,0 +1,81 @@
+info:
+ name: Get Kobo Library
+ type: http
+ seq: 6
+
+http:
+ method: GET
+ url: '{{base_url}}/api/sync/kobo/library'
+ auth: inherit
+
+docs: |-
+ ## Get Kobo Library
+
+ Retrieves the user's Kobo library metadata for device sync.
+
+ **Method:** GET
+
+ **Endpoint:** /api/sync/kobo/library
+
+ **Authentication:** Bearer token
+
+ **Query Parameters:**
+ None (returns entire library)
+
+ **Response:**
+ - `books` (array): Library items
+ - `ContentId` (string): Unique book identifier
+ - `Title` (string): Book title
+ - `Author` (string): Author name
+ - `Publisher` (string): Publisher name
+ - `Description` (string): Book description
+ - `ISBN` (string, optional): ISBN-13
+ - `PublicationDate` (string): Release date
+ - `EntitlementId` (string): Kobo entitlement ID
+ - `CrossRevisionId` (string): Revision identifier
+ - `MimeType` (string): Content type (application/epub+zip)
+ - `FileSize` (integer): File size in bytes
+ - `CoverImageId` (string): Cover image identifier
+ - `DownloadUrls` (object): Download URLs
+ - `download_url` (string): Direct download link
+ - `download_acquisition_url` (string): OPDS acquisition URL
+ - `sync_metadata` (object):
+ - `last_sync` (string): Last library sync timestamp
+ - `total_books` (integer): Total book count
+ - `has_updates` (boolean): Whether updates are available
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Device not approved
+
+ **Library Sync Purpose:**
+ - Kobo device needs to know available books
+ - Enables download via device browser
+ - Provides metadata for device display
+ - Supports Kobo's "My Books" feature
+ - Enables on-device purchasing integration
+
+ **Kobo Device Usage:**
+ - Device fetches library on registration
+ - Refreshed daily or on manual sync
+ - User can browse library on device
+ - Books downloaded wirelessly to device
+ - Supports "Buy on Kobo, read on device" workflow
+
+ **Authentication Methods:**
+ This endpoint uses **Bearer token authentication** (token in Authorization header).
+ Alternative: Use `/sync/kobo/{token}/library` with token in URL path.
+
+ **Performance:**
+ - Typical response: 50-200KB for 100 books
+ - Processing time: 200-800ms
+ - Cache duration: 5 minutes
+ - Pagination available for large libraries (>500 books)
+
+ **Use Cases:**
+ - Initial device registration
+ - Library refresh on device
+ - Book discovery on device
+ - Download link generation
+ - Metadata sync for OPDS
diff --git a/bruno-yaml/devices/kobo/Get Unlinked Book Suggestions.yml b/bruno-yaml/devices/kobo/Get Unlinked Book Suggestions.yml
new file mode 100644
index 0000000..4350ba0
--- /dev/null
+++ b/bruno-yaml/devices/kobo/Get Unlinked Book Suggestions.yml
@@ -0,0 +1,22 @@
+info:
+ name: Get Unlinked Book Suggestions
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/sync/unlinked-books/{{unlinkedBookId}}/suggestions'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Get Unlinked Book Suggestions
+
+ Retrieves suggested media items from the library that match an unlinked book, enabling manual linking.
+
+ **Method:** GET
+
+ **Endpoint:** /sync/unlinked-books/{unlinkedBookId
diff --git a/bruno-yaml/devices/kobo/api.yml b/bruno-yaml/devices/kobo/api.yml
new file mode 100644
index 0000000..16eea94
--- /dev/null
+++ b/bruno-yaml/devices/kobo/api.yml
@@ -0,0 +1,7 @@
+info:
+ name: Bookhoard Kobo Sync API
+ type: collection
+ seq: 1
+http:
+ method: POST
+ url: '"http://localhost:8765/api"'
diff --git a/bruno-yaml/devices/kobo/scenarios/Auto Link Books.yml b/bruno-yaml/devices/kobo/scenarios/Auto Link Books.yml
new file mode 100644
index 0000000..558f42b
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Auto Link Books.yml
@@ -0,0 +1,45 @@
+info:
+ name: Auto-Link Unlinked Books
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/sync/auto-link-books'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"confidence_threshold\": 0.8,\n \"limit\": 50"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Auto-Link Unlinked Books
+
+ Automatically links unlinked books to media items based on title and author matching with a configurable confidence threshold.
+
+ **Method:** POST
+
+ **Endpoint:** /sync/auto-link-books
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `confidence_threshold` (number, optional): Minimum confidence score for auto-linking (0-1, default: 0.8)
+ - `limit` (number, optional): Maximum number of books to auto-link (default: 50)
+
+ **Response:**
+ - `results` (array): Results for each auto-link attempt
+ - `total` (number): Total number of books processed
+ - `success` (number): Number of successful links
+ - `failed` (number): Number of failed links
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Note:** Higher confidence thresholds produce fewer but more accurate matches. Consider the tradeoff between automation and accuracy.
diff --git a/bruno-yaml/devices/kobo/scenarios/Bulk Link Books.yml b/bruno-yaml/devices/kobo/scenarios/Bulk Link Books.yml
new file mode 100644
index 0000000..acc6fc5
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Bulk Link Books.yml
@@ -0,0 +1,47 @@
+info:
+ name: Bulk Link Unlinked Books
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/sync/bulk-link-books'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"links\": [\n {\n \"unlinked_book_id\": \"{{unlinkedBookId1"
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: Authorization
+ value: Bearer {{authToken
+
+docs: |-
+ ## Bulk Link Unlinked Books
+
+ Links multiple unlinked books to media items in a single request.
+
+ **Method:** POST
+
+ **Endpoint:** /sync/bulk-link-books
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `links` (array): Array of link objects
+ - `unlinked_book_id` (string): Unlinked book UUID
+ - `media_item_id` (string): Media item UUID to link to
+ - `confidence_score` (number): Match confidence (0-1)
+
+ **Response:**
+ - `results` (array): Results for each link attempt
+ - `total` (number): Total number of links processed
+ - `success` (number): Number of successful links
+ - `failed` (number): Number of failed links
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Note:** Use this endpoint after reviewing suggestions from the Get Unlinked Book Suggestions endpoint.
diff --git a/bruno-yaml/devices/kobo/scenarios/Kobo Initialization.yml b/bruno-yaml/devices/kobo/scenarios/Kobo Initialization.yml
new file mode 100644
index 0000000..cb7f87d
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Kobo Initialization.yml
@@ -0,0 +1,98 @@
+info:
+ name: Kobo Initialization
+ type: http
+ seq: 8
+
+http:
+ method: GET
+ url: '{{base_url}}/api/sync/kobo/v1/initialization'
+ auth: inherit
+
+docs: |-
+ ## Kobo Device Initialization
+
+ Initializes Kobo device sync session and returns device configuration.
+
+ **Method:** GET
+
+ **Endpoint:** /api/sync/kobo/v1/initialization
+
+ **Authentication:** Bearer token
+
+ **Query Parameters:**
+ - `Platform` (string, optional): Device platform (android, kobo)
+ - `FirmwareVersion` (string, optional): Firmware version string
+ - `Model` (string, optional): Device model name
+
+ **Response:**
+ - `device` (object): Device information
+ - `device_id` (string): Server device ID
+ - `approved` (boolean): Whether device is approved
+ - `sync_enabled` (boolean): Whether sync is active
+ - `last_sync` (string): Last successful sync timestamp
+ - `user` (object): User information
+ - `user_id` (string): User identifier
+ - `email` (string): User email (masked)
+ - `library_size` (integer): Number of books in library
+ - `sync_config` (object): Sync configuration
+ - `sync_interval_minutes` (integer): Recommended sync frequency
+ - `batch_size` (integer): Max items per batch
+ - `timeout_seconds` (integer): Request timeout
+ - `retry_count` (integer): Max retry attempts
+ - `features` (object): Available features
+ - `annotation_sync` (boolean): Annotation support
+ - `bookmark_sync` (boolean): Bookmark support
+ - `progress_sync` (boolean): Progress tracking
+ - `library_download` (boolean): Library access
+ - `endpoints` (object): API endpoint URLs
+ - `markup_sync` (string): Progress/annotation sync URL
+ - `bookmark_sync` (string): Bookmark sync URL
+ - `library` (string): Library access URL
+ - `sync_from_server` (string): Download sync URL
+ - `server_time` (string): Current server timestamp
+ - `version` (string): API version
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized (invalid token)
+ - 403: Forbidden (device not approved)
+ - 404: Device not found
+
+ **Initialization Flow:**
+ 1. Device powers on or connects to network
+ 2. Device calls initialization endpoint
+ 3. Server returns configuration and capabilities
+ 4. Device adjusts sync behavior based on config
+ 5. Device begins sync operations
+
+ **Authentication Methods:**
+ This endpoint uses **Bearer token authentication** (token in Authorization header).
+ Alternative: Use `/sync/kobo/{token}/v1/initialization` with token in URL path.
+
+ **Configuration Caching:**
+ - Response cached on device for 24 hours
+ - Refreshed on device reboot
+ - Updated when sync settings change
+ - Can be force-refreshed via device settings
+
+ **Device Approval:**
+ - New devices: `approved: false`
+ - Pending devices see limited functionality
+ - Approval required for full sync
+ - User approves via web interface
+ - Re-initialization after approval
+
+ **Use Cases:**
+ - Device registration
+ - Daily device wakeup
+ - Post-approval initialization
+ - Configuration refresh
+ - Feature capability check
+ - Sync endpoint discovery
+
+ **Kobo Native Integration:**
+ - Called by Kobo Nickel UI
+ - Integrated with Kobo sync service
+ - Part of Kobo account setup
+ - Supports Kobo "Sync now" feature
+ - Enables Kobo library browsing
diff --git a/bruno-yaml/devices/kobo/scenarios/Server Sync to Kobo.yml b/bruno-yaml/devices/kobo/scenarios/Server Sync to Kobo.yml
new file mode 100644
index 0000000..e53609b
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Server Sync to Kobo.yml
@@ -0,0 +1,53 @@
+info:
+ name: Sync from Bookhoard to Kobo
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{baseURL}}/api/sync/kobo/sync-from-server'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "[\n {\n \"ContentId\": \"{{bookUUID"
+ headers:
+ - key: Authorization
+ value: Bearer {{koboToken
+
+docs: |-
+ ## Sync from Bookhoard to Kobo
+
+ Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device. Two-way sync endpoint.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/kobo/sync-from-server
+
+ **Authentication:** Bearer token with Kobo device identification
+
+ **Headers:**
+ - `x-kobo-device` (string): JSON string containing Kobo device info
+ - `DeviceId`: Kobo device ID
+ - `Model`: Kobo device model
+ - `SerialNumber`: Kobo device serial number
+
+ **Request Body:** Array of sync data objects
+ - `ContentId` (string): Book UUID
+ - `PercentRead` (number): Reading progress percentage (0-100)
+ - `LastModified` (string): ISO 8601 timestamp
+ - `Bookmarks` (array, optional): Array of bookmark objects
+ - `BookmarkId`: Unique bookmark ID
+ - `ContentId`: Book UUID
+ - `BookmarkText`: Bookmark text/note
+ - `BookmarkType`: Type (bookmark, annotation, etc.)
+ - `BookmarkTitle`: Bookmark title
+ - `Highlights` (array, optional): Array of highlight objects (same structure as bookmarks)
+
+ **Response:**
+ - Sync result confirmation
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Note:** Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights.
diff --git a/bruno-yaml/devices/kobo/scenarios/Sync Books from Server to Kobo.yml b/bruno-yaml/devices/kobo/scenarios/Sync Books from Server to Kobo.yml
new file mode 100644
index 0000000..d41a5e2
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Sync Books from Server to Kobo.yml
@@ -0,0 +1,106 @@
+info:
+ name: Sync Books from Server to Kobo
+ type: http
+ seq: 9
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/kobo/sync-from-server'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ force_sync: true
+ books:
+ - book-uuid-1
+ - book-uuid-2
+
+docs: |-
+ ## Sync Books from Server to Kobo
+
+ Pulls reading progress, annotations, and bookmarks from server to Kobo device.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/kobo/sync-from-server
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `force_sync` (boolean): Force full sync (ignore last sync time)
+ - `books` (array, optional): List of book UUIDs to sync
+ - If empty, syncs all books with server data
+ - If specified, only syncs listed books
+ - `sync_options` (object, optional):
+ - `since_date` (string): ISO 8601 timestamp for incremental sync
+ - `include_annotations` (boolean): Include highlights/notes
+ - `include_progress` (boolean): Include reading progress
+ - `conflict_resolution` (string): `most_recent`, `server_wins`, `client_wins`
+
+ **Response:**
+ - `synced` (integer): Number of books synced
+ - `skipped` (integer): Books with no server changes
+ - `failed` (integer): Books that failed to sync
+ - `results` (array): Per-book sync results
+ - `book_id` (string): Book UUID
+ - `status` (string): `synced`, `skipped`, `failed`
+ - `progress_pulled` (boolean): Whether progress was downloaded
+ - `annotations_pulled` (integer): Number of annotations downloaded
+ - `error` (string, optional): Error message if failed
+ - `server_timestamp` (string): Server timestamp of sync
+
+ **Status Codes:**
+ - 200: Sync completed
+ - 207: Multi-status (some succeeded, some failed)
+ - 401: Unauthorized
+ - 400: Invalid request
+
+ **Sync Direction:**
+ - **Server → Device** (this endpoint)
+ - Device → Server: Use `/sync/kobo/markup` endpoint
+ - Bidirectional sync achieved by calling both
+
+ **Pull Sync Use Cases:**
+ - New device setup (download all progress)
+ - Device replacement (restore from server)
+ - Multi-device sync (pull changes from other devices)
+ - Conflict resolution (server wins)
+ - Manual "Download from server" operation
+
+ **Conflict Resolution:**
+ - `most_recent`: Latest timestamp wins (default)
+ - `server_wins`: Server data always used
+ - `client_wins`: Device data preserved
+ - Applied per-book, per-item
+
+ **Sync Optimization:**
+ - Incremental sync by default (since last sync)
+ - Force sync does full comparison
+ - Book-level batching (10 books per batch)
+ - Delta transfer (only changed items)
+ - Compression for large annotation sets
+
+ **Kobo Device Behavior:**
+ - Device updates local database
+ - Progress reflected in reading view
+ - Annotations appear in Notebook
+ - Bookmarks updated in navigation
+ - Conflict warnings shown to user
+ - Sync progress displayed on screen
+
+ **Performance:**
+ - Small sync (1-10 books): 2-5 seconds
+ - Medium sync (10-50 books): 5-15 seconds
+ - Large sync (50-200 books): 15-45 seconds
+ - Timeout: 120 seconds
+
+ **Use Cases:**
+ - Initial device sync
+ - After firmware update
+ - From another device's changes
+ - Manual sync request
+ - Conflict recovery
+ - Data restoration
diff --git a/bruno-yaml/devices/kobo/scenarios/Sync Multiple Books Progress.yml b/bruno-yaml/devices/kobo/scenarios/Sync Multiple Books Progress.yml
new file mode 100644
index 0000000..c983710
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Sync Multiple Books Progress.yml
@@ -0,0 +1,91 @@
+info:
+ name: Sync Multiple Books Progress
+ type: http
+ seq: 3
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/kobo/markup'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: x-kobo-device
+ value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Aura"}'
+ body:
+ type: json
+ json:
+ ReadingSync:
+ - ContentId: book-1-uuid
+ PercentRead: 25.0
+ EntitlementId: entitlement-1
+ RemainingTimeMinutes: 240
+ FirstReadTime: '2026-01-25T10:00:00Z'
+ LastModified: '2026-01-30T18:00:00Z'
+ - ContentId: book-2-uuid
+ PercentRead: 78.5
+ EntitlementId: entitlement-2
+ RemainingTimeMinutes: 45
+ FirstReadTime: '2026-01-25T14:00:00Z'
+ LastModified: '2026-01-30T20:00:00Z'
+ BookmarkSync: []
+
+docs: |-
+ ## Sync Multiple Books Progress
+
+ Synchronizes reading progress for multiple books in a single request.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/kobo/markup
+
+ **Authentication:** Bearer token
+
+ **Headers:**
+ - `Authorization`: Bearer {{kobo_device_token}}
+ - `x-kobo-device`: Device information (Model: "Kobo Aura")
+
+ **Request Body:**
+ - `ReadingSync` (array): Multiple progress items
+ - Each item contains: ContentId, PercentRead, EntitlementId, etc.
+ - `BookmarkSync` (array): Empty for progress-only batch
+
+ **Response:**
+ - `total` (integer): Total items in request
+ - `synced` (integer): Successfully synced
+ - `failed` (integer): Failed items
+ - `results` (array): Per-item results
+ - `ContentId` (string): Book UUID
+ - `status` (string): `synced`, `failed`, `skipped`
+ - `error` (string, optional): Error message if failed
+
+ **Status Codes:**
+ - 200: Batch sync completed
+ - 207: Multi-status (some succeeded, some failed)
+ - 401: Unauthorized
+ - 413: Payload too large (>1MB)
+
+ **Batch Sync Advantages:**
+ - Efficient sync of entire library
+ - Reduces HTTP overhead
+ - Faster for devices with many books
+ - Atomic operation (all or nothing by default)
+
+ **Kobo Batch Sync Behavior:**
+ - Triggered when device connects after being offline
+ - Occurs during manual "Sync now" operation
+ - Limited to 100 books per request
+ - Progress updates shown on device
+ - Failed items retried individually
+
+ **Performance:**
+ - Typical batch: 10-50 books in 1-3 seconds
+ - Large batch: 50-100 books in 3-8 seconds
+ - Timeout: 30 seconds
+ - Rate limit: 10 batches per minute per device
+
+ **Use Cases:**
+ - Initial device sync after registration
+ - Catch-up sync after extended offline period
+ - Library-wide progress update
+ - Pre-sync before device firmware update
diff --git a/bruno-yaml/devices/kobo/scenarios/Sync Progress with Bookmarks.yml b/bruno-yaml/devices/kobo/scenarios/Sync Progress with Bookmarks.yml
new file mode 100644
index 0000000..9afdeca
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Sync Progress with Bookmarks.yml
@@ -0,0 +1,84 @@
+info:
+ name: Sync Progress with Bookmarks
+ type: http
+ seq: 2
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/kobo/markup'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: x-kobo-device
+ value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
+ body:
+ type: json
+ json:
+ ReadingSync:
+ - ContentId: book-uuid
+ PercentRead: 42.3
+ EntitlementId: entitlement-id
+ RemainingTimeMinutes: 138
+ FirstReadTime: '2026-01-25T10:00:00Z'
+ LastModified: '2026-01-30T20:00:00Z'
+ BookmarkSync:
+ - ContentId: book-uuid
+ BookmarkText: highlighted text passage
+ BookmarkType: annotation
+ BookmarkTitle: Chapter 3
+
+docs: |-
+ ## Sync Progress with Bookmarks
+
+ Synchronizes reading progress and highlights/annotations from Kobo device.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/kobo/markup
+
+ **Authentication:** Bearer token
+
+ **Headers:**
+ - `Authorization`: Bearer {{kobo_device_token}}
+ - `x-kobo-device`: Device information JSON
+
+ **Request Body:**
+ - `ReadingSync` (array): Progress items (see Sync Reading Progress)
+ - `BookmarkSync` (array): Highlights and annotations
+ - `ContentId` (string): Book UUID
+ - `BookmarkText` (string): Highlighted text or note content
+ - `BookmarkType` (string): Type of bookmark
+ - `annotation`: Highlighted text
+ - `note`: Personal note
+ - `bookmark`: Location bookmark
+ - `BookmarkTitle` (string): Reference (e.g., chapter name)
+ - `ChapterID` (string, optional): Chapter identifier
+ - `DateCreated` (string, optional): Creation timestamp
+
+ **Response:**
+ - `progress_synced` (integer): Progress items synced
+ - `bookmarks_synced` (integer): Bookmark items synced
+ - `conflicts_resolved` (integer): Number of conflicts auto-resolved
+ - `timestamp` (string): Sync timestamp
+ - `details` (object): Sync breakdown by type
+
+ **Status Codes:**
+ - 200: Successful sync
+ - 401: Unauthorized
+ - 400: Invalid data
+
+ **Kobo Highlight Features:**
+ - 5 highlight colors (yellow, green, blue, pink, orange)
+ - Chapter-based organization
+ - Linked to reading progress
+ - Appears in Kobo "Notebook" view
+ - Can be exported from device
+ - Syncs across all user devices
+
+ **Annotation Sync:**
+ - Highlight text preserved exactly
+ - Color mapped to system colors
+ - Chapter reference maintained
+ - Location data converted to standard format
+ - Notes attached to highlights synced separately
diff --git a/bruno-yaml/devices/kobo/scenarios/Sync Reading Progress.yml b/bruno-yaml/devices/kobo/scenarios/Sync Reading Progress.yml
new file mode 100644
index 0000000..9770d42
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Sync Reading Progress.yml
@@ -0,0 +1,84 @@
+info:
+ name: Sync Reading Progress
+ type: http
+ seq: 1
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/kobo/markup'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: x-kobo-device
+ value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
+ body:
+ type: json
+ json:
+ ReadingSync:
+ - ContentId: book-uuid-here
+ PercentRead: 45.6
+ EntitlementId: entitlement-id-here
+ RemainingTimeMinutes: 120
+ FirstReadTime: '2026-01-25T10:00:00Z'
+ LastModified: '2026-01-30T20:00:00Z'
+ BookmarkSync: []
+
+docs: |-
+ ## Sync Kobo Reading Progress
+
+ Synchronizes reading progress from a Kobo device to the server using Bearer token authentication.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/kobo/markup
+
+ **Authentication:** Bearer token (in Authorization header)
+
+ **Headers:**
+ - `Authorization`: Bearer {{kobo_device_token}}
+ - `x-kobo-device`: JSON-encoded device info
+ - `DeviceId`: Kobo device identifier
+ - `Model`: Device model (e.g., "Kobo Clara", "Kobo Libra", "Kobo Aura")
+
+ **Request Body:**
+ - `ReadingSync` (array): Reading progress items
+ - `ContentId` (string): Book/Content UUID
+ - `PercentRead` (number): Reading progress 0-100
+ - `EntitlementId` (string): Kobo entitlement ID
+ - `RemainingTimeMinutes` (integer): Estimated reading time remaining
+ - `FirstReadTime` (string): ISO 8601 timestamp when first opened
+ - `LastModified` (string): ISO 8601 timestamp of last progress update
+ - `BookmarkSync` (array): Empty array for progress-only sync
+
+ **Response:**
+ - `synced` (integer): Number of items synced
+ - `failed` (integer): Number of items that failed to sync
+ - `timestamp` (string): Server timestamp of sync
+ - `books` (array): Synced book data
+ - `ContentId` (string): Book UUID
+ - `status` (string): `synced`, `failed`, `skipped`
+ - `server_percent` (number): Server-side progress (for conflict detection)
+
+ **Status Codes:**
+ - 200: Sync successful
+ - 401: Invalid device token
+ - 403: Device not approved
+ - 400: Invalid request format
+
+ **Authentication Methods:**
+ This endpoint uses **Bearer token authentication** (token in Authorization header).
+ Alternative: Use `/sync/kobo/{token}/markup` with token in URL path.
+
+ **Kobo Sync Features:**
+ - Native Kobo sync protocol
+ - Supports Kobo Clara, Libra, Aura, Forma, Sage, Elipsa
+ - Progress percentage tracking
+ - Reading time estimation
+ - Cross-device synchronization
+ - Automatic conflict resolution (most recent wins)
+
+ **Sync Frequency:**
+ - Kobo devices auto-sync every 15-30 minutes when connected to WiFi
+ - Manual sync available from device settings
+ - Sync triggers on: device wake, book close, WiFi connection
diff --git a/bruno-yaml/devices/kobo/scenarios/Sync Single Bookmark.yml b/bruno-yaml/devices/kobo/scenarios/Sync Single Bookmark.yml
new file mode 100644
index 0000000..dfbc92f
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Sync Single Bookmark.yml
@@ -0,0 +1,92 @@
+info:
+ name: Sync Single Bookmark
+ type: http
+ seq: 5
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/kobo/bookmark'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: x-kobo-device
+ value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
+ body:
+ type: json
+ json:
+ ContentId: book-uuid
+ BookmarkText: Bookmarked passage
+ BookmarkType: annotation
+ BookmarkTitle: Chapter 3
+
+docs: |-
+ ## Sync Single Bookmark
+
+ Synchronizes an individual bookmark/highlight from Kobo device.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/kobo/bookmark
+
+ **Authentication:** Bearer token
+
+ **Headers:**
+ - `Authorization`: Bearer {{kobo_device_token}}
+ - `x-kobo-device`: Device information JSON
+
+ **Request Body:**
+ - `ContentId` (string): Book UUID
+ - `BookmarkText` (string): Highlighted text or bookmark description
+ - `BookmarkType` (string): Type of bookmark
+ - `annotation`: Highlighted text passage
+ - `note`: Personal note
+ - `bookmark`: Location marker
+ - `BookmarkTitle` (string): Reference title (e.g., chapter name)
+ - `ChapterID` (string, optional): Chapter identifier
+ - `DateCreated` (string, optional): ISO 8601 timestamp
+ - `highlight_color` (string, optional): Color name (yellow, green, blue, pink, orange)
+
+ **Response:**
+ - `id` (string): Server bookmark ID
+ - `ContentId` (string): Associated book UUID
+ - `status` (string): `created`, `updated`, `skipped` (duplicate)
+ - `timestamp` (string): Server timestamp
+ - `url` (string): API URL to retrieve bookmark
+
+ **Status Codes:**
+ - 201: Bookmark created
+ - 200: Bookmark updated (duplicate found)
+ - 409: Duplicate bookmark (unchanged)
+ - 401: Unauthorized
+ - 400: Invalid bookmark data
+
+ **Single Bookmark Sync vs Batch:**
+ - **Single bookmark endpoint:** Real-time, immediate sync
+ - **Batch markup endpoint:** Deferred, periodic sync
+ - Use single when user explicitly creates highlight
+ - Use batch for periodic background sync
+
+ **Kobo Trigger:**
+ - User highlights text → immediate single sync
+ - User adds note → immediate single sync
+ - Device goes online → batch sync of all changes
+
+ **Duplicate Detection:**
+ - Same ContentId + similar BookmarkText + same location
+ - Updates existing if text modified
+ - Skips if identical bookmark exists
+ - Preserves creation date of original
+
+ **Use Cases:**
+ - Real-time highlight sync
+ - Instant note backup
+ - Immediate annotation sharing
+ - Quick single annotation update
+ - Testing annotation sync
+
+ **Notes:**
+ - Much faster than full markup sync
+ - Lower bandwidth usage
+ - Ideal for intermittent connectivity
+ - Can be called multiple times safely
diff --git a/bruno-yaml/devices/kobo/scenarios/Sync with Annotations.yml b/bruno-yaml/devices/kobo/scenarios/Sync with Annotations.yml
new file mode 100644
index 0000000..f8518d7
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/Sync with Annotations.yml
@@ -0,0 +1,106 @@
+info:
+ name: Sync with Annotations
+ type: http
+ seq: 4
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/kobo/markup'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ - key: x-kobo-device
+ value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Libra"}'
+ body:
+ type: json
+ json:
+ ReadingSync:
+ - ContentId: book-uuid
+ PercentRead: 55.0
+ EntitlementId: entitlement-id
+ RemainingTimeMinutes: 120
+ FirstReadTime: '2026-01-25T10:00:00Z'
+ LastModified: '2026-01-30T20:00:00Z'
+ BookmarkSync:
+ - ContentId: book-uuid
+ BookmarkText: Important quote
+ BookmarkType: annotation
+ BookmarkTitle: Chapter 4 - The Truth
+ - ContentId: book-uuid
+ BookmarkText: Another quote
+ BookmarkType: annotation
+ BookmarkTitle: Chapter 5
+ - ContentId: book-uuid
+ BookmarkText: Note to myself
+ BookmarkType: note
+ BookmarkTitle: Personal note
+
+docs: |-
+ ## Sync with Multiple Annotations
+
+ Synchronizes reading progress with multiple highlights and notes.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/kobo/markup
+
+ **Authentication:** Bearer token
+
+ **Headers:**
+ - `Authorization`: Bearer {{kobo_device_token}}
+ - `x-kobo-device`: Device info (Model: "Kobo Libra")
+
+ **Request Body:**
+ - `ReadingSync` (array): Single book progress
+ - `BookmarkSync` (array): Multiple annotations
+ - Can include highlights (annotation type)
+ - Can include notes (note type)
+ - Each has: ContentId, BookmarkText, BookmarkType, BookmarkTitle
+
+ **Response:**
+ - `progress_synced` (boolean): Progress update status
+ - `annotations_synced` (integer): Number of annotations synced
+ - `highlights_count` (integer): Highlights synced
+ - `notes_count` (integer): Notes synced
+ - `conflicts` (array): Any annotation conflicts resolved
+ - `timestamp` (string): Sync completion time
+
+ **Status Codes:**
+ - 200: Successful sync
+ - 401: Unauthorized
+ - 400: Invalid annotation data
+
+ **Kobo Annotation Types:**
+ - **Highlights:** Selected text passages
+ - 5 preset colors available
+ - Can have chapter titles
+ - Exportable to PDF/Mobile
+ - **Notes:** Personal annotations
+ - Free-form text
+ - Can be attached to highlights
+ - Separate from highlights
+ - **Bookmarks:** Location markers
+ - Chapter positions
+ - Quick navigation
+
+ **Sync Behavior:**
+ - Duplicates detected by content matching
+ - Most recent edit wins conflicts
+ - Annotations linked to book content
+ - Chapter references preserved
+ - Order maintained from device
+
+ **Kobo Notebook Export:**
+ - All annotations appear in Kobo "Notebook"
+ - Can be exported to PDF
+ - Can be exported to Mobile (text)
+ - Organized by book
+ - Shows highlight context
+
+ **Use Cases:**
+ - Study and research
+ - Book club discussion prep
+ - Content review
+ - Sharing insights
+ - Personal learning archive
diff --git a/bruno-yaml/devices/kobo/scenarios/bookmark-sync.yml b/bruno-yaml/devices/kobo/scenarios/bookmark-sync.yml
new file mode 100644
index 0000000..a472f5f
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/bookmark-sync.yml
@@ -0,0 +1,45 @@
+info:
+ name: Kobo Bookmark Sync
+ type: http
+ seq: 3
+http:
+ method: POST
+ url: '{{base_url}}/api/v1/kobo/bookmark'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"BookmarkSync\": [\n {\n \"BookmarkId\": \"bookmark_2\"\
+ ,\n \"ContentId\": \"kobo_xyz789\",\n \"BookmarkText\": \"Important\
+ \ note\",\n \"BookmarkType\": \"bookmark\",\n \"DateCreated\"\
+ : \"2026-01-31T12:00:00Z\""
+ headers:
+ - key: Authorization
+ value: Bearer {{device_token
+
+docs: |-
+ ## Kobo Bookmark Sync
+
+ Synchronizes bookmarks from a Kobo device to the Bookhoard server.
+
+ **Method:** POST
+
+ **Endpoint:** /api/v1/kobo/bookmark
+
+ **Authentication:** Bearer token (device token)
+
+ **Request Body:**
+ - `BookmarkSync` (array): Array of bookmark objects
+ - `BookmarkId` (string): Unique bookmark ID
+ - `ContentId` (string): Book/content ID
+ - `BookmarkText` (string): Bookmark text or note
+ - `BookmarkType` (string): Type (bookmark, highlight, note)
+ - `DateCreated` (string): ISO 8601 timestamp
+
+ **Response:**
+ - `Status` (string): Sync status (Success, Partial)
+ - `BookmarksSynced` (number): Number of bookmarks synced
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/kobo/scenarios/get-initialization-url-token.yml b/bruno-yaml/devices/kobo/scenarios/get-initialization-url-token.yml
new file mode 100644
index 0000000..30e851f
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/get-initialization-url-token.yml
@@ -0,0 +1,19 @@
+info:
+ name: Kobo Initialization - URL Path Token
+ type: http
+ seq: 4
+http:
+ method: GET
+ url: '{{base_url}}/sync/kobo/{{kobo_device_token}}/v1/initialization'
+ auth: none
+ body:
+ type: none
+
+docs: |-
+ ## Kobo Initialization - URL Path Token
+
+ Returns initialization data for Kobo device using token in URL path.
+
+ **Method:** GET
+
+ **Endpoint:** /sync/kobo/{kobo_device_token
diff --git a/bruno-yaml/devices/kobo/scenarios/get-library-url-token.yml b/bruno-yaml/devices/kobo/scenarios/get-library-url-token.yml
new file mode 100644
index 0000000..b04b101
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/get-library-url-token.yml
@@ -0,0 +1,19 @@
+info:
+ name: Get Library - URL Path Token
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/sync/kobo/{{kobo_device_token}}/library'
+ auth: none
+ body:
+ type: none
+
+docs: |-
+ ## Get Kobo Library - URL Path Token
+
+ Retrieves library metadata for Kobo device using token in URL path.
+
+ **Method:** GET
+
+ **Endpoint:** /sync/kobo/{kobo_device_token
diff --git a/bruno-yaml/devices/kobo/scenarios/get-unlinked-books.yml b/bruno-yaml/devices/kobo/scenarios/get-unlinked-books.yml
new file mode 100644
index 0000000..219fda7
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/get-unlinked-books.yml
@@ -0,0 +1,39 @@
+info:
+ name: Get Unlinked Books - User View
+ type: http
+ seq: 4
+http:
+ method: GET
+ url: '{{base_url}}/api/sync/unlinked-books'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{user_token
+
+docs: |-
+ ## Get Unlinked Books - User View
+
+ Retrieves all unlinked books for the authenticated user that need manual linking.
+
+ **Method:** GET
+
+ **Endpoint:** /api/sync/unlinked-books
+
+ **Authentication:** Bearer token
+
+ **Response:**
+ - `unlinked` (array): Array of unlinked book objects
+ - `id` (string): Unlinked book UUID
+ - `title` (string): Book title
+ - `author` (string): Book author
+ - `device_id` (string): Source device ID
+ - `device_name` (string): Source device name
+ - `detected_at` (string): Detection timestamp
+ - `total` (number): Total count of unlinked books
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/kobo/scenarios/initialization.yml b/bruno-yaml/devices/kobo/scenarios/initialization.yml
new file mode 100644
index 0000000..f65fd09
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/initialization.yml
@@ -0,0 +1,34 @@
+info:
+ name: Kobo Initialization
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/v1/kobo/initialization'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{device_token
+
+docs: |-
+ ## Kobo Initialization
+
+ Initializes Kobo device sync, returning device resources and account information.
+
+ **Method:** GET
+
+ **Endpoint:** /api/v1/kobo/initialization
+
+ **Authentication:** Bearer token (device token)
+
+ **Response:**
+ - `ContentId` (string): Device content ID
+ - `Categories` (array): Available categories/collections
+ - `BookhoardUUID` (string): Bookhoard instance UUID
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/kobo/scenarios/link-book.yml b/bruno-yaml/devices/kobo/scenarios/link-book.yml
new file mode 100644
index 0000000..b6f3564
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/link-book.yml
@@ -0,0 +1,43 @@
+info:
+ name: Link Unlinked Book - Manual Resolution
+ type: http
+ seq: 5
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/link-book'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"unlinked_book_id\": \"{{unlinked_book_id"
+ headers:
+ - key: Authorization
+ value: Bearer {{user_token
+
+docs: |-
+ ## Link Unlinked Book - Manual Resolution
+
+ Manually links an unlinked book to a media item in the library.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/link-book
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `unlinked_book_id` (string): Unlinked book UUID
+ - `media_item_id` (string): Media item UUID to link to
+ - `confidence_score` (number): Match confidence (0-1, 1.0 for manual)
+
+ **Response:**
+ - `status` (string): Link status (linked)
+ - `unlinked_book_id` (string): Unlinked book UUID
+ - `media_item_id` (string): Media item UUID
+ - `message` (string): Success message
+
+ **Status Codes:**
+ - 200: Success - book linked
+ - 400: Invalid request
+ - 401: Unauthorized
+ - 404: Book or media item not found
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/kobo/scenarios/markup-sync.yml b/bruno-yaml/devices/kobo/scenarios/markup-sync.yml
new file mode 100644
index 0000000..cbc01b2
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/markup-sync.yml
@@ -0,0 +1,52 @@
+info:
+ name: Kobo Markup Sync
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/v1/kobo/markup'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"kobo_abc123def456\"\
+ ,\n \"PercentRead\": 60.0,\n \"RemainingTimeMin\": 120,\n \
+ \ \"ReadingEvent\": \"BookRead\",\n \"LastModified\": \"2026-01-31T12:00:00Z\""
+ headers:
+ - key: Authorization
+ value: Bearer {{device_token
+
+docs: |-
+ ## Kobo Markup Sync
+
+ Synchronizes reading progress and markup (highlights, bookmarks) from a Kobo device.
+
+ **Method:** POST
+
+ **Endpoint:** /api/v1/kobo/markup
+
+ **Authentication:** Bearer token (device token)
+
+ **Request Body:**
+ - `ReadingSync` (array, optional): Reading progress data
+ - `ContentId` (string): Book/content ID
+ - `PercentRead` (number): Percentage read (0-100)
+ - `RemainingTimeMin` (number): Remaining time in minutes
+ - `ReadingEvent` (string): Event type (BookRead, etc.)
+ - `LastModified` (string): ISO 8601 timestamp
+ - `BookmarkSync` (array, optional): Bookmark/highlight data
+ - `BookmarkId` (string): Unique bookmark ID
+ - `ContentId` (string): Book/content ID
+ - `BookmarkText` (string): Highlighted/bookmarked text
+ - `BookmarkType` (string): Type (annotation, bookmark)
+ - `DateCreated` (string): ISO 8601 timestamp
+ - `Metadata` (boolean): Whether to include metadata
+
+ **Response:**
+ - `Status` (string): Sync status (Success, Partial)
+ - `MarkupsSynced` (number): Number of markups synced
+ - `BookmarksSynced` (number): Number of bookmarks synced
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/kobo/scenarios/sync-bookmark-url-token.yml b/bruno-yaml/devices/kobo/scenarios/sync-bookmark-url-token.yml
new file mode 100644
index 0000000..95bd6dd
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/sync-bookmark-url-token.yml
@@ -0,0 +1,25 @@
+info:
+ name: Sync Bookmark - URL Path Token
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/sync/kobo/{{kobo_device_token}}/bookmark'
+ auth: none
+ body:
+ type: json
+ jsonBody: "{\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Highlighted\
+ \ text\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"\
+ Chapter 3\""
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Sync Bookmark - URL Path Token
+
+ Synchronizes bookmarks and annotations from Kobo device using token in URL path.
+
+ **Method:** POST
+
+ **Endpoint:** /sync/kobo/{kobo_device_token
diff --git a/bruno-yaml/devices/kobo/scenarios/sync-markup-url-token.yml b/bruno-yaml/devices/kobo/scenarios/sync-markup-url-token.yml
new file mode 100644
index 0000000..1f711d3
--- /dev/null
+++ b/bruno-yaml/devices/kobo/scenarios/sync-markup-url-token.yml
@@ -0,0 +1,26 @@
+info:
+ name: Sync Markup - URL Path Token
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/sync/kobo/{{kobo_device_token}}/markup'
+ auth: none
+ body:
+ type: json
+ jsonBody: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\"\
+ ,\n \"PercentRead\": 45.6,\n \"EntitlementId\": \"entitlement-id\"\
+ ,\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\"\
+ ,\n \"LastModified\": \"2026-01-30T20:00:00Z\""
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Sync Reading Progress - URL Path Token
+
+ Synchronizes reading progress from Kobo device using token in URL path.
+
+ **Method:** POST
+
+ **Endpoint:** /sync/kobo/{kobo_device_token
diff --git a/bruno-yaml/devices/koreader/Get Book Metadata.yml b/bruno-yaml/devices/koreader/Get Book Metadata.yml
new file mode 100644
index 0000000..97ac7f3
--- /dev/null
+++ b/bruno-yaml/devices/koreader/Get Book Metadata.yml
@@ -0,0 +1,92 @@
+info:
+ name: Get Book Metadata
+ type: http
+ seq: 8
+
+http:
+ method: GET
+ url: '{{base_url}}/api/sync/koreader/metadata/{{book_uuid}}'
+ auth: inherit
+
+docs: |-
+ ## Get KOReader Book Metadata
+
+ Retrieves metadata for a specific book from the server.
+
+ **Method:** GET
+
+ **Endpoint:** /api/sync/koreader/metadata/:book_uuid
+
+ **Authentication:** Bearer token
+
+ **Path Parameters:**
+ - `book_uuid` (string): SHA-256 based book identifier
+
+ **Response:**
+ - `uuid` (string): Book UUID (SHA-256)
+ - `title` (string): Book title
+ - `authors` (array): Author names
+ - `series` (string, optional): Series name
+ - `series_index` (number, optional): Position in series
+ - `publisher` (string, optional): Publisher name
+ - `publication_date` (string, optional): Release date
+ - `language` (string, optional): ISO 639-1 language code
+ - `description` (string, optional): Book description
+ - `cover_url` (string, optional): Cover image URL
+ - `cover_thumbnail_url` (string, optional): Thumbnail URL
+ - `identifiers` (object): Various identifiers
+ - `isbn` (string, optional): ISBN-13
+ - `asin` (string, optional): Amazon ASIN
+ - `goodreads` (string, optional): Goodreads ID
+ - `google_books` (string, optional): Google Books ID
+ - `metadata_sources` (array): Sources metadata was pulled from
+ - `last_synced` (string): Last metadata sync timestamp
+ - `file_metadata` (object):
+ - `file_size` (integer): File size in bytes
+ - `format` (string): File format (epub, pdf, mobi, etc.)
+ - `pages` (integer, optional): Page count
+ - `word_count` (integer, optional): Estimated words
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Book not found
+
+ **Metadata Purpose:**
+ - Enrich book information on device
+ - Improve book organization
+ - Enable better search
+ - Support series sorting
+ - Provide cover images
+ - Link to external sources
+
+ **KOReader Usage:**
+ - Display in book info dialog
+ - Used for library sorting
+ - Shown in file browser
+ - Series grouping
+ - Cover display
+ - Search optimization
+
+ **SHA-256 Book ID:**
+ - Primary identifier in KOReader
+ - Universal across devices
+ - Format-independent
+ - Generated from file content
+ - Survives metadata changes
+
+ **Metadata Sources:**
+ - Google Books API
+ - Open Library
+ - Goodreads API
+ - ISBN database lookup
+ - Publisher metadata
+ - User-provided metadata
+
+ **Use Cases:**
+ - Initial book import
+ - Metadata refresh
+ - Cover image download
+ - Series organization
+ - Duplicate detection
+ - Library management
diff --git a/bruno-yaml/devices/koreader/Get Library.yml b/bruno-yaml/devices/koreader/Get Library.yml
new file mode 100644
index 0000000..3aeaf35
--- /dev/null
+++ b/bruno-yaml/devices/koreader/Get Library.yml
@@ -0,0 +1,34 @@
+info:
+ name: KOReader Get Library
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/sync/koreader/library'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{device_token
+
+docs: |-
+ ## KOReader Get Library
+
+ Retrieves the user's library for KOReader sync operations.
+
+ **Method:** GET
+
+ **Endpoint:** /api/sync/koreader/library
+
+ **Authentication:** Bearer token (device token)
+
+ **Response:**
+ - `library_sync` (object): Library sync data
+ - `total_books` (number): Total number of books
+ - `books` (array): Array of book objects
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/koreader/Get User Library for KOReader.yml b/bruno-yaml/devices/koreader/Get User Library for KOReader.yml
new file mode 100644
index 0000000..740b387
--- /dev/null
+++ b/bruno-yaml/devices/koreader/Get User Library for KOReader.yml
@@ -0,0 +1,103 @@
+info:
+ name: Get User Library for KOReader
+ type: http
+ seq: 9
+
+http:
+ method: GET
+ url: '{{base_url}}/api/sync/koreader/library'
+ auth: inherit
+
+docs: |-
+ ## Get KOReader User Library
+
+ Retrieves the user's book library for KOReader device sync.
+
+ **Method:** GET
+
+ **Endpoint:** /api/sync/koreader/library
+
+ **Authentication:** Bearer token (koreader_device_token)
+
+ **Query Parameters:**
+ None (returns entire library)
+
+ **Response:**
+ - `books` (array): Library items
+ - `uuid` (string): SHA-256 based book identifier
+ - `title` (string): Book title
+ - `authors` (array): Author names
+ - `series` (string, optional): Series name
+ - `series_index` (number, optional): Series position
+ - `publisher` (string, optional): Publisher
+ - `publication_date` (string, optional): Release date
+ - `language` (string, optional): Language code
+ - `description` (string, optional): Description
+ - `cover_url` (string, optional): Cover image URL
+ - `cover_thumbnail_url` (string, optional): Thumbnail URL
+ - `file_metadata` (object):
+ - `format` (string): File format
+ - `file_size` (integer): Size in bytes
+ - `pages` (integer, optional): Page count
+ - `progress` (object, optional): Reading progress
+ - `percentage` (number): Progress 0-100
+ - `last_read` (string): Last read timestamp
+ - `epubcfi` (string): Current position
+ - `annotations_count` (object, optional): Annotation stats
+ - `highlights` (integer): Highlight count
+ - `notes` (integer): Note count
+ - `bookmarks` (integer): Bookmark count
+ - `library_info` (object):
+ - `total_books` (integer): Total book count
+ - `last_sync` (string): Last library sync timestamp
+ - `has_updates` (boolean): Updates available
+ - `user_info` (object):
+ - `user_id` (string): User identifier
+ - `email` (string): Email (masked)
+ - `libraries` (array): Available library IDs
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Device not approved
+
+ **Library Purpose for KOReader:**
+ - Discover books available for download
+ - Browse catalog on device
+ - Sync reading progress across books
+ - Download covers/metadata
+ - Series-based organization
+ - Cloud library access
+
+ **KOReader Device Features:**
+ - **OPDS catalog**: Native OPDS client
+ - **File browser**: See server books
+ - **Cloud download**: Download books on-demand
+ - **Metadata sync**: Automatic metadata fetching
+ - **Cover images**: Display in library view
+ - **Progress sync**: See progress across all books
+ - **Search**: Search library by title/author
+
+ **KOReader-Specific Features:**
+ - SHA-256 based book IDs
+ - Multi-format support (EPUB, FB2, PDF, DJVU, MOBI, etc.)
+ - Series sorting and grouping
+ - Language filtering
+ - Cover image caching
+ - Metadata for file browser enhancement
+ - Integration with KOReader's OPDS client
+
+ **Performance:**
+ - Typical response: 100-500KB for 100 books
+ - Processing time: 300ms-1s
+ - Cache duration: 5 minutes
+ - Pagination support for libraries >500 books
+
+ **Use Cases:**
+ - Initial device setup
+ - Library browsing on device
+ - Book download
+ - Metadata refresh
+ - Cover image sync
+ - Progress overview
+ - Series-based reading
diff --git a/bruno-yaml/devices/koreader/api.yml b/bruno-yaml/devices/koreader/api.yml
new file mode 100644
index 0000000..aa14f3b
--- /dev/null
+++ b/bruno-yaml/devices/koreader/api.yml
@@ -0,0 +1,7 @@
+info:
+ name: Bookhoard KOReader Sync API
+ type: collection
+ seq: 1
+http:
+ method: POST
+ url: '"http://localhost:8765/api"'
diff --git a/bruno-yaml/devices/koreader/scenarios/Checkpoint Sync.yml b/bruno-yaml/devices/koreader/scenarios/Checkpoint Sync.yml
new file mode 100644
index 0000000..d8f241d
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Checkpoint Sync.yml
@@ -0,0 +1,123 @@
+info:
+ name: Checkpoint Sync
+ type: http
+ seq: 11
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/progress'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ sync_mode: checkpoint
+ checkpoint_id: checkpoint-uuid
+ since_timestamp: '2026-01-30T19:00:00Z'
+ books:
+ - uuid: book-uuid-1
+ percentage: 0.45
+ chapter: 3
+ - uuid: book-uuid-2
+ percentage: 0.75
+ chapter: 8
+
+docs: |-
+ ## Checkpoint Sync
+
+ Incremental sync using checkpoint-based change tracking.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/progress
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `sync_mode` (string): Must be `checkpoint`
+ - `checkpoint_id` (string): Unique checkpoint identifier
+ - `since_timestamp` (string): ISO 8601 timestamp for incremental sync
+ - `books` (array): Book progress items
+ - `uuid` (string): Book UUID
+ - `percentage` (number): Progress 0.0-1.0
+ - `chapter` (integer): Current chapter
+ - `epubcfi` (string, optional): Current position
+ - `modified_since` (boolean, optional): Whether modified since checkpoint
+
+ **Response:**
+ - `checkpoint_id` (string): Server checkpoint ID
+ - `checkpoint_timestamp` (string): Checkpoint creation time
+ - `processed` (integer): Books processed
+ - `changes_only` (boolean): Whether only changed items synced
+ - `next_checkpoint_id` (string): ID for next checkpoint sync
+ - `results` (array): Per-book results
+
+ **Status Codes:**
+ - 200: Checkpoint sync completed
+ - 401: Unauthorized
+ - 400: Invalid checkpoint or timestamp
+
+ **Checkpoint Sync Benefits:**
+ - **Incremental**: Only sync changed items
+ - **Efficient**: Smaller payloads
+ - **Fast**: Reduced processing time
+ - **Reliable**: Checkpoint-based tracking
+ - **Resumable**: Can continue from last checkpoint
+
+ **Checkpoint Mechanism:**
+ - Server tracks changes since checkpoint
+ - Client provides checkpoint ID or timestamp
+ - Only modified books returned/processed
+ - Checkpoint ID advances on each sync
+ - Supports large libraries efficiently
+
+ **Use Cases:**
+ - Large libraries (100+ books)
+ - Intermittent connectivity
+ - Bandwidth optimization
+ - Battery conservation
+ - Background sync
+ - Periodic sync (every 5-15 minutes)
+
+ **Checkpoint Lifecycle:**
+ 1. Initial sync: No checkpoint (full sync)
+ 2. Server returns checkpoint_id
+ 3. Next sync: Client sends checkpoint_id
+ 4. Server processes only changes
+ 5. New checkpoint_id returned
+ 6. Repeat from step 3
+
+ **Sync Optimization:**
+ - Only books with progress changes
+ - Skips unmodified books
+ - Delta transfer
+ - Compression for large payloads
+ - Batch processing
+
+ **Failure Handling:**
+ - Checkpoint ID preserved on failure
+ - Retry with same checkpoint
+ - Full sync if checkpoint expired
+ - Checkpoint validity: 24 hours
+ - Auto-fallback to full sync
+
+ **Performance:**
+ - Small changes (1-10 books): 100-300ms
+ - Medium changes (10-50 books): 300ms-1s
+ - Large changes (50-100 books): 1-3s
+ - Typical: 5-10x faster than full sync
+
+ **When to Use:**
+ - Default sync mode for most users
+ - Periodic background sync
+ - Large library management
+ - Mobile/network-constrained environments
+ - Battery-powered devices
+
+ **Configuration:**
+ - Checkpoint expiration: 24 hours
+ - Max history: 100 checkpoints
+ - Auto-cleanup of old checkpoints
+ - Configurable sync interval
diff --git a/bruno-yaml/devices/koreader/scenarios/Immediate Sync - Page Turn.yml b/bruno-yaml/devices/koreader/scenarios/Immediate Sync - Page Turn.yml
new file mode 100644
index 0000000..2aa56e2
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Immediate Sync - Page Turn.yml
@@ -0,0 +1,105 @@
+info:
+ name: Immediate Sync - Page Turn
+ type: http
+ seq: 10
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/progress'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ sync_mode: immediate
+ books:
+ - uuid: book-uuid
+ percentage: 0.45678
+ chapter: 3
+ timestamp: '2026-01-30T20:00:00Z'
+
+docs: |-
+ ## Immediate Sync - Page Turn
+
+ Real-time progress sync triggered immediately on page turn.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/progress
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `sync_mode` (string): Must be `immediate`
+ - `books` (array): Current book progress
+ - `uuid` (string): Book UUID
+ - `percentage` (number): Precise progress (0.45678 = 45.678%)
+ - `chapter` (integer): Current chapter
+ - `timestamp` (string): ISO 8601 timestamp
+ - `epubcfi` (string, optional): Current position
+ - `page` (integer, optional): Current page
+
+ **Response:**
+ - `synced` (boolean): Immediate sync status
+ - `timestamp` (string): Server timestamp
+ - `next_sync_suggested` (string): Suggested next sync time
+
+ **Status Codes:**
+ - 200: Sync queued
+ - 202: Accepted for processing
+ - 401: Unauthorized
+ - 429: Too many immediate sync requests (rate limited)
+
+ **Immediate Sync Mode:**
+ - **Purpose**: Real-time progress updates
+ - **Trigger**: Every page turn (configurable)
+ - **Priority**: High priority processing
+ - **Latency**: <100ms typical
+ - **Best effort**: May be queued under load
+
+ **Rate Limiting:**
+ - Max 60 requests per minute per device
+ - Throttled after limit reached
+ - Suggests switching to periodic sync
+ - Prevents server overload
+
+ **Use Cases:**
+ - Real-time multi-device reading
+ - Live progress sharing
+ - Instant position backup
+ - Critical reading points
+ - Test/profiling mode
+
+ **Performance:**
+ - Ultra-fast sync
+ - Minimal payload
+ - Optimized for speed
+ - Async processing
+ - No confirmation wait
+
+ **Battery Considerations:**
+ - More frequent network use
+ - Higher battery consumption
+ - WiFi recommended
+ - Can reduce sync frequency in settings
+
+ **Configuration:**
+ - Can enable/disable per device
+ - Adjustable frequency (every page, every N pages)
+ - Automatic fallback to periodic sync on error
+ - Respects device power-save mode
+
+ **When to Use:**
+ - Critical reading sessions
+ - Multi-device concurrent reading
+ - Research and study
+ - Testing sync functionality
+ - When power source available
+
+ **When NOT to Use:**
+ - Battery conservation needed
+ - Unstable network
+ - Extended reading sessions
+ - Background sync preferred
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Annotations (Per-Book SHA-256).yml b/bruno-yaml/devices/koreader/scenarios/Sync Annotations (Per-Book SHA-256).yml
new file mode 100644
index 0000000..66e273d
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Annotations (Per-Book SHA-256).yml
@@ -0,0 +1,44 @@
+info:
+ name: KOReader Sync Annotations - Per-Book SHA-256
+ type: http
+ seq: 4
+http:
+ method: POST
+ url: '{{base_url}}/api/v1/koreader/sync/bookmarks'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"book_uuid\": \"{{book_uuid"
+ headers:
+ - key: Authorization
+ value: Bearer {{koreader_device_token
+
+docs: |-
+ ## KOReader Sync Annotations - Per-Book SHA-256
+
+ Synchronizes annotations (highlights) from KOReader with per-annotation SHA-256 hashes for multi-book sync.
+
+ **Method:** POST
+
+ **Endpoint:** /api/v1/koreader/sync/bookmarks
+
+ **Authentication:** Bearer token (KOReader device token)
+
+ **Request Body:**
+ - `book_uuid` (string): Primary book UUID
+ - `highlights` (array): Array of highlight objects
+ - `text` (string): Highlighted text
+ - `pos0`, `pos1` (string): EPUB CFI positions
+ - `color` (string): Highlight color (hex)
+ - `page` (number): Page number
+ - `book_sha256` (string): SHA-256 hash for this specific book
+
+ **Response:**
+ - `highlights_synced` (number): Number of highlights synced
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Note:** Each highlight can include its own book_sha256, allowing annotations from multiple books in a single request.
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Bookmarks (SHA-256).yml b/bruno-yaml/devices/koreader/scenarios/Sync Bookmarks (SHA-256).yml
new file mode 100644
index 0000000..5120533
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Bookmarks (SHA-256).yml
@@ -0,0 +1,40 @@
+info:
+ name: KOReader Sync Bookmarks - SHA-256
+ type: http
+ seq: 3
+http:
+ method: POST
+ url: '{{base_url}}/api/v1/koreader/sync/bookmarks'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"book_sha256\": \"{{book_sha256"
+ headers:
+ - key: Authorization
+ value: Bearer {{koreader_device_token
+
+docs: |-
+ ## KOReader Sync Bookmarks - SHA-256
+
+ Synchronizes bookmarks, notes, and highlights from KOReader using SHA-256 hash for book identification.
+
+ **Method:** POST
+
+ **Endpoint:** /api/v1/koreader/sync/bookmarks
+
+ **Authentication:** Bearer token (KOReader device token)
+
+ **Request Body:**
+ - `book_sha256` (string): SHA-256 hash of book file
+ - `bookmarks` (array): Array of bookmarks
+ - `notes` (array): Array of notes
+ - `highlights` (array): Array of highlights
+
+ **Response:**
+ - `sync_status` (string): Sync status
+ - `total_synced` (number): Total items synced
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Bookmarks.yml b/bruno-yaml/devices/koreader/scenarios/Sync Bookmarks.yml
new file mode 100644
index 0000000..7f15311
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Bookmarks.yml
@@ -0,0 +1,105 @@
+info:
+ name: Sync Bookmarks
+ type: http
+ seq: 5
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/bookmarks'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ library_id: optional-library-uuid
+ books:
+ - uuid: book-uuid
+ bookmarks:
+ - chapter: 3
+ datetime: '2026-01-30T19:55:00Z'
+ notes: Marked this chapter as important
+ pos0: 'epubcfi(/6/4/2:15)'
+ pos1: 'epubcfi(/6/4/2:20)'
+ page: 45
+ text: Important passage
+ type: bookmark
+
+docs: |-
+ ## Sync KOReader Bookmarks
+
+ Synchronizes bookmarks separately from progress for KOReader.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/bookmarks
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `library_id` (string, optional): Library UUID
+ - `books` (array): Books with bookmarks
+ - `uuid` (string): Book UUID
+ - `bookmarks` (array): Bookmark items
+ - `chapter` (integer): Chapter number
+ - `datetime` (string): ISO 8601 timestamp
+ - `notes` (string): Bookmark description
+ - `pos0` (string): EPUB CFI start position
+ - `pos1` (string): EPUB CFI end position
+ - `page` (integer): Page number
+ - `text` (string): Displayed text
+ - `type` (string): `bookmark`, `highlight`, or `note`
+
+ **Response:**
+ - `synced` (integer): Number of bookmarks synced
+ - `duplicates_skipped` (integer): Duplicate bookmarks skipped
+ - `results` (array): Per-bookmark results
+ - `timestamp` (string): Sync timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 400: Invalid data
+
+ **Dedicated Bookmark Endpoint:**
+ - **Purpose**: Sync bookmarks independently
+ - **Use case**: More frequent bookmark updates
+ - **Advantage**: Separate from progress sync
+ - **Efficiency**: Smaller payloads
+
+ **KOReader Bookmark Features:**
+ - Chapter-based organization
+ - Quick navigation markers
+ - Hierarchical bookmarks (via plugins)
+ - Custom bookmark titles
+ - Date/time tracking
+ - EPUB CFI precision
+
+ **Bookmark Types in KOReader:**
+ - **Location bookmarks**: Quick navigation points
+ - **Chapter marks**: Auto-generated chapter markers
+ - **Progress bookmarks**: Last read positions
+ - **Custom bookmarks**: User-created markers
+ - **Search bookmarks**: Saved search results
+
+ **Sync Behavior:**
+ - Duplicate detection by position + text
+ - Most recent wins on conflicts
+ - Chapter order preserved
+ - Auto-generated vs manual bookmarks differentiated
+ - Merge with existing bookmarks
+
+ **KOReader Device Integration:**
+ - Created via "Add bookmark" menu
+ - Shown in "Bookmarks" panel
+ - Quick access via "Go to bookmark"
+ - Exportable to JSON
+ - Can be edited/deleted
+
+ **Use Cases:**
+ - Quick navigation aids
+ - Chapter markers
+ - Important passages
+ - Reading progress points
+ - Study session markers
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Highlights.yml b/bruno-yaml/devices/koreader/scenarios/Sync Highlights.yml
new file mode 100644
index 0000000..746894b
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Highlights.yml
@@ -0,0 +1,110 @@
+info:
+ name: Sync Highlights
+ type: http
+ seq: 6
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/highlights'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ library_id: optional-library-uuid
+ books:
+ - uuid: book-uuid
+ highlights:
+ - datetime: '2026-01-30T19:50:00Z'
+ text: Important quote from book
+ chapter: 4
+ pos0: 'epubcfi(/6/4/2:20)'
+ pos1: 'epubcfi(/6/4/2:30)'
+ page_start: 78
+ page_end: 79
+
+docs: |-
+ ## Sync KOReader Highlights
+
+ Synchronizes text highlights separately from other annotations.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/highlights
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `library_id` (string, optional): Library UUID
+ - `books` (array): Books with highlights
+ - `uuid` (string): Book UUID
+ - `highlights` (array): Highlight items
+ - `datetime` (string): Creation timestamp
+ - `text` (string): Highlighted text content
+ - `chapter` (integer): Chapter number
+ - `pos0` (string): EPUB CFI start position
+ - `pos1` (string): EPUB CFI end position
+ - `page_start` (integer): Start page
+ - `page_end` (integer): End page
+ - `color` (string, optional): Color name or hex
+ - `note` (string, optional): Attached note
+
+ **Response:**
+ - `synced` (integer): Highlights synced
+ - `duplicates_skipped` (integer): Duplicates found
+ - `with_notes` (integer): Highlights that have notes attached
+ - `timestamp` (string): Sync timestamp
+
+ **KOReader Highlight Features:**
+ - Precise text selection (EPUB CFI)
+ - Custom color support (via plugins)
+ - Multi-page highlights
+ - Chapter references
+ - Timestamps for sorting
+ - Attached notes support
+ - Full text preservation
+
+ **Highlight Colors (via plugins):**
+ - Yellow (default): General highlighting
+ - Green: Important concepts
+ - Blue: Key information
+ - Red/pink: Critical content
+ - Orange: Interesting quotes
+ - Custom RGB colors available
+
+ **EPUB CFI Advantages:**
+ - Precise start/end positions
+ - Works across font size changes
+ - Survives text reflow
+ - Device-independent
+ - Standardized format
+
+ **Dedicated Highlight Endpoint:**
+ - **Separate from progress**: Sync highlights independently
+ - **Smaller payload**: Just highlights, no progress
+ - **More frequent**: Can sync highlights immediately
+ - **Focused**: Single-purpose endpoint
+
+ **Sync Behavior:**
+ - Exact text matching for duplicates
+ - Position-based conflict resolution
+ - Color preservation across devices
+ - Note attachments preserved
+ - Order maintained by position
+
+ **KOReader Device Features:**
+ - Created via long-press or selection
+ - Color picker available
+ - Can add notes immediately
+ - Shows in "Highlights" panel
+ - Exportable to Evernote, etc.
+ - Searchable by content
+
+ **Use Cases:**
+ - Study and research
+ - Content curation
+ - Quote collection
+ - Academic work
+ - Sharing insights
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Notes.yml b/bruno-yaml/devices/koreader/scenarios/Sync Notes.yml
new file mode 100644
index 0000000..578548c
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Notes.yml
@@ -0,0 +1,108 @@
+info:
+ name: Sync Notes
+ type: http
+ seq: 7
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/notes'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ library_id: optional-library-uuid
+ books:
+ - uuid: book-uuid
+ notes:
+ - datetime: '2026-01-30T19:52:00Z'
+ text: My personal note about this chapter
+ chapter: 4
+
+docs: |-
+ ## Sync KOReader Notes
+
+ Synchronizes user notes separately from highlights and bookmarks.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/notes
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `library_id` (string, optional): Library UUID
+ - `books` (array): Books with notes
+ - `uuid` (string): Book UUID
+ - `notes` (array): Note items
+ - `datetime` (string): Creation/modification timestamp
+ - `text` (string): Note content
+ - `chapter` (integer): Chapter number
+ - `pos0` (string, optional): Related EPUB CFI position
+ - `page` (integer, optional): Page number
+ - `highlighted_text` (string, optional): Related highlight
+
+ **Response:**
+ - `synced` (integer): Notes synced
+ - `duplicates_skipped` (integer): Duplicate notes found
+ - `attached_to_highlights` (integer): Notes linked to highlights
+ - `timestamp` (string): Sync timestamp
+
+ **KOReader Note Features:**
+ - Free-form text notes
+ - Can be standalone or attached to highlights
+ - Chapter-based organization
+ - Timestamped for sorting
+ - Markdown support (some versions)
+ - Longer form content
+ - No character limit
+
+ **Note Types:**
+ - **Standalone notes**: Independent notes about chapter/section
+ - **Attached notes**: Notes attached to specific highlights
+ - **Chapter notes**: Notes about entire chapter
+ - **Book notes**: General notes about the book
+
+ **Dedicated Notes Endpoint:**
+ - **Separate sync**: Notes sync independently
+ - **Flexible**: Not tied to highlights
+ - **Efficient**: Smaller, focused payload
+ - **Immediate**: Can sync right after note creation
+
+ **Attached Notes:**
+ - Linked to specific highlight
+ - Shares highlight's position
+ - Shown together with highlight
+ - Deleted when highlight deleted (optional)
+ - Color matches highlight
+
+ **Sync Behavior:**
+ - Text-based duplicate detection
+ - Time-based conflict resolution
+ - Chapter organization preserved
+ - Markdown formatting preserved
+ - Attachment links maintained
+
+ **KOReader Device Integration:**
+ - Created via "Add note" option
+ - Edited in note editor
+ - Shown in "Notes" panel
+ - Can be organized by chapter
+ - Export functionality available
+ - Search support
+
+ **Advanced Features:**
+ - **Markdown**: Bold, italic, lists (some versions)
+ - **Tags**: User-defined tags (via plugins)
+ - **Links**: Internal/external links
+ - **Formatting**: Rich text in newer versions
+
+ **Use Cases:**
+ - Study notes
+ - Personal reflections
+ - Academic annotations
+ - Research insights
+ - Book club discussion prep
+ - Knowledge management
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Progress (SHA-256 Only).yml b/bruno-yaml/devices/koreader/scenarios/Sync Progress (SHA-256 Only).yml
new file mode 100644
index 0000000..b71294b
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Progress (SHA-256 Only).yml
@@ -0,0 +1,47 @@
+info:
+ name: KOReader Sync Progress - SHA-256 Only
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/v1/koreader/sync/progress'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \
+ \ \"sha256\": \"{{book_sha256"
+ headers:
+ - key: Authorization
+ value: Bearer {{koreader_device_token
+
+docs: |-
+ ## KOReader Sync Progress - SHA-256 Only
+
+ Synchronizes reading progress using only SHA-256 hash for book identification (when UUID is not available).
+
+ **Method:** POST
+
+ **Endpoint:** /api/v1/koreader/sync/progress
+
+ **Authentication:** Bearer token (KOReader device token)
+
+ **Request Body:**
+ - `sync_mode` (string): Sync mode (immediate, deferred)
+ - `books` (array): Array of book progress objects
+ - `sha256` (string): SHA-256 hash of book file
+ - `file_path` (string): Path to book file
+ - `percentage` (number): Progress percentage
+ - `page` (number): Current page
+ - `total_pages` (number): Total pages
+
+ **Response:**
+ - `sync_status` (string): Sync status
+ - `books_synced` (number): Number of books synced
+
+ **Status Codes:**
+ - 200: Success
+ - 202: Accepted
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Note:** Use this when book UUID is not available, falling back to SHA-256 hash for identification.
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Progress (SHA-256).yml b/bruno-yaml/devices/koreader/scenarios/Sync Progress (SHA-256).yml
new file mode 100644
index 0000000..24d3ef1
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Progress (SHA-256).yml
@@ -0,0 +1,51 @@
+info:
+ name: KOReader Sync Progress - SHA-256
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/v1/koreader/sync/progress'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \
+ \ \"uuid\": \"{{book_uuid"
+ headers:
+ - key: Authorization
+ value: Bearer {{koreader_device_token
+
+docs: |-
+ ## KOReader Sync Progress - SHA-256
+
+ Synchronizes reading progress from a KOReader device using SHA-256 book hash for identification.
+
+ **Method:** POST
+
+ **Endpoint:** /api/v1/koreader/sync/progress
+
+ **Authentication:** Bearer token (KOReader device token)
+
+ **Request Body:**
+ - `sync_mode` (string): Sync mode (immediate, deferred)
+ - `books` (array): Array of book progress objects
+ - `uuid` (string): Book UUID
+ - `sha256` (string): SHA-256 hash of book file for identification
+ - `file_path` (string): Path to book file on device
+ - `percentage` (number): Progress percentage (0-1)
+ - `chapter` (number): Current chapter
+ - `page` (number): Current page
+ - `total_pages` (number): Total pages
+ - `epubcfi` (string): EPUB location
+ - `last_read` (string): ISO 8601 timestamp
+ - `title` (string): Book title
+ - `authors` (array): List of authors
+
+ **Response:**
+ - `sync_status` (string): Sync status
+ - `books_synced` (number): Number of books synced
+
+ **Status Codes:**
+ - 200: Success
+ - 202: Accepted - processing
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Progress - Multiple Books.yml b/bruno-yaml/devices/koreader/scenarios/Sync Progress - Multiple Books.yml
new file mode 100644
index 0000000..dc10032
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Progress - Multiple Books.yml
@@ -0,0 +1,90 @@
+info:
+ name: Sync Progress - Multiple Books
+ type: http
+ seq: 2
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/progress'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ library_id: optional-library-uuid
+ books:
+ - uuid: book-1-uuid
+ title: Book One
+ authors:
+ - Author One
+ progress: 0.25
+ percentage: 0.25
+ last_read: '2026-01-30T19:00:00Z'
+ chapter: 1
+ epubcfi: 'epubcfi(/6/4/2:10)'
+ - uuid: book-2-uuid
+ title: Book Two
+ authors:
+ - Author Two
+ progress: 0.75
+ percentage: 0.75
+ last_read: '2026-01-30T20:00:00Z'
+ chapter: 8
+ epubcfi: 'epubcfi(/6/4/2:50)'
+
+docs: |-
+ ## Sync KOReader Progress - Multiple Books
+
+ Synchronizes reading progress for multiple books in a single request.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/progress
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `library_id` (string, optional): Library UUID
+ - `books` (array): Multiple book progress items
+ - Each item contains: uuid, title, authors, progress, percentage, last_read, chapter, epubcfi
+
+ **Response:**
+ - `synced` (integer): Number of books successfully synced
+ - `failed` (integer): Number of books that failed
+ - `results` (array): Per-book sync results
+ - `timestamp` (string): Sync timestamp
+
+ **Status Codes:**
+ - 200: Batch sync completed
+ - 207: Multi-status (partial success)
+ - 401: Unauthorized
+ - 413: Payload too large
+
+ **Batch Sync Advantages:**
+ - Efficient sync of entire library
+ - Single HTTP request for multiple books
+ - Faster periodic sync
+ - Reduced battery usage vs individual syncs
+ - Better for batch processing
+
+ **KOReader Batch Behavior:**
+ - Triggered on device wake from sleep
+ - Occurs during "Sync now" operation
+ - Runs on WiFi connection
+ - Limited to 100 books per request
+ - Automatic retry on failed books
+
+ **Performance:**
+ - Small batch (2-10 books): 200-500ms
+ - Medium batch (10-50 books): 500ms-2s
+ - Large batch (50-100 books): 2-5s
+ - Timeout: 30 seconds
+
+ **Use Cases:**
+ - Device initialization sync
+ - Periodic background sync
+ - Post-offline catch-up sync
+ - Library-wide progress update
+ - Before firmware update
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Progress - Single Book.yml b/bruno-yaml/devices/koreader/scenarios/Sync Progress - Single Book.yml
new file mode 100644
index 0000000..ee76d5c
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Progress - Single Book.yml
@@ -0,0 +1,92 @@
+info:
+ name: Sync Progress - Single Book
+ type: http
+ seq: 1
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/progress'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ library_id: optional-library-uuid
+ books:
+ - uuid: book-uuid-here
+ title: Book Title
+ authors:
+ - Author Name
+ progress: 0.45
+ percentage: 0.45
+ last_read: '2026-01-30T20:00:00Z'
+ chapter: 3
+ epubcfi: 'epubcfi(/6/4/2:15)'
+ character: 15432
+
+docs: |-
+ ## Sync KOReader Progress - Single Book
+
+ Synchronizes reading progress for a single book from KOReader device.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/progress
+
+ **Authentication:** Bearer token (koreader_device_token)
+
+ **Request Body:**
+ - `library_id` (string, optional): Library UUID for multi-library setups
+ - `books` (array): Array with single book progress
+ - `uuid` (string): Unique book identifier (often SHA-256 hash)
+ - `title` (string): Book title
+ - `authors` (array): List of authors
+ - `progress` (number): Progress decimal (0.0 to 1.0)
+ - `percentage` (number): Progress percentage (0.45 = 45%)
+ - `last_read` (string): ISO 8601 timestamp of last read
+ - `chapter` (integer): Current chapter number
+ - `epubcfi` (string): EPUB Canonical Fragment Identifier
+ - `character` (integer): Character position in book
+ - `page` (integer, optional): Current page number
+
+ **Response:**
+ - `synced` (integer): Number of books synced
+ - `timestamp` (string): Server sync timestamp
+ - `books` (array): Sync results
+ - `uuid` (string): Book UUID
+ - `status` (string): `synced`, `updated`, `skipped`
+ - `server_progress` (object): Server-side progress data
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 400: Invalid request format
+
+ **KOReader Progress Tracking:**
+ - SHA-256 based book identification (universal across devices)
+ - EPUB CFI for precise location (standard format)
+ - Chapter-based navigation
+ - Character-level precision
+ - Supports EPUB, FB2, PDF, DJVU, MOBI formats
+
+ **EPUB CFI Format:**
+ - Standardized location format for EPUBs
+ - Example: `epubcfi(/6/4/2:15)`
+ - Identifies exact position even after reflow
+ - Works across different devices/apps
+ - Preserved after file modifications
+
+ **Book Identification:**
+ - Primary: SHA-256 hash of book file
+ - Universal: Same book = same UUID across devices
+ - Format-agnostic: Works for any supported format
+ - Case-sensitive: Hash must match exactly
+
+ **Use Cases:**
+ - Real-time page turn sync
+ - Progress backup
+ - Cross-device continuity
+ - Reading time tracking
+ - Chapter completion detection
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Progress - With Bookmarks.yml b/bruno-yaml/devices/koreader/scenarios/Sync Progress - With Bookmarks.yml
new file mode 100644
index 0000000..bf48c4d
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Progress - With Bookmarks.yml
@@ -0,0 +1,106 @@
+info:
+ name: Sync Progress - With Bookmarks
+ type: http
+ seq: 3
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/progress'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ library_id: optional-library-uuid
+ books:
+ - uuid: book-uuid-here
+ title: Book Title
+ authors:
+ - Author Name
+ progress: 0.45
+ percentage: 0.45
+ last_read: '2026-01-30T20:00:00Z'
+ bookmarks:
+ - chapter: 3
+ datetime: '2026-01-30T19:55:00Z'
+ notes: highlighted text
+ pos0: 'epubcfi(/6/4/2:15)'
+ pos1: 'epubcfi(/6/4/2:20)'
+ page: 45
+ text: highlighted text excerpt
+ type: highlight
+
+docs: |-
+ ## Sync Progress with Bookmarks
+
+ Synchronizes reading progress with bookmarks/highlights from KOReader.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/progress
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `library_id` (string, optional): Library UUID
+ - `books` (array): Books with progress and bookmarks
+ - `uuid`, `title`, `authors`, `progress`, etc.
+ - `bookmarks` (array): Bookmark items
+ - `chapter` (integer): Chapter number
+ - `datetime` (string): ISO 8601 timestamp
+ - `notes` (string): Note content or highlighted text
+ - `pos0` (string): EPUB CFI start position
+ - `pos1` (string): EPUB CFI end position
+ - `page` (integer): Page number
+ - `text` (string): Displayed text excerpt
+ - `type` (string): `highlight`, `bookmark`, or `note`
+
+ **Response:**
+ - `progress_synced` (integer): Progress items synced
+ - `bookmarks_synced` (integer): Bookmark items synced
+ - `highlights_synced` (integer): Highlight count
+ - `timestamp` (string): Sync timestamp
+
+ **KOReader Bookmark Types:**
+ - **highlight**: Selected text passages
+ - **bookmark**: Location markers
+ - **note**: Text annotations (can be attached to highlights)
+
+ **KOReader Highlight Features:**
+ - Custom colors (via color extensions)
+ - Multi-color support
+ - Precise EPUB CFI positioning
+ - Chapter-based organization
+ - Text excerpts preserved
+ - Date/time stamped
+ - Page number tracking
+
+ **EPUB CFI in Bookmarks:**
+ - `pos0`: Start position (highlight start)
+ - `pos1`: End position (highlight end)
+ - Exact text selection boundaries
+ - Survives text reflow
+ - Works across different font sizes
+
+ **Sync Behavior:**
+ - Duplicate detection by text + position
+ - Most recent edit wins
+ - Chapter references maintained
+ - Order preserved from device
+ - Merges with existing server bookmarks
+
+ **KOReader Device Integration:**
+ - Created in KOReader highlight interface
+ - Shows in "Bookmarks" menu
+ - Can be edited/deleted on device
+ - Exportable to JSON/XML
+ - Searchable by content
+
+ **Use Cases:**
+ - Study and research
+ - Content review
+ - Passage tracking
+ - Quick navigation
+ - Cross-device bookmark access
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Progress - With Highlights and Notes.yml b/bruno-yaml/devices/koreader/scenarios/Sync Progress - With Highlights and Notes.yml
new file mode 100644
index 0000000..975b9c8
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Progress - With Highlights and Notes.yml
@@ -0,0 +1,117 @@
+info:
+ name: Sync Progress - With Highlights and Notes
+ type: http
+ seq: 4
+
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/progress'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ library_id: optional-library-uuid
+ books:
+ - uuid: book-uuid-here
+ title: Book Title
+ authors:
+ - Author Name
+ progress: 0.6
+ percentage: 0.6
+ last_read: '2026-01-30T20:00:00Z'
+ highlights:
+ - datetime: '2026-01-30T19:50:00Z'
+ text: Important passage
+ chapter: 4
+ pos0: 'epubcfi(/6/4/2:20)'
+ pos1: 'epubcfi(/6/4/2:30)'
+ page_start: 78
+ page_end: 79
+ notes:
+ - datetime: '2026-01-30T19:52:00Z'
+ text: My note about this chapter
+ chapter: 4
+
+docs: |-
+ ## Sync Progress with Highlights and Notes
+
+ Synchronizes reading progress with separate highlights and notes arrays.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/progress
+
+ **Authentication:** Bearer token
+
+ **Request Body:**
+ - `library_id` (string, optional): Library UUID
+ - `books` (array): Books with progress and annotations
+ - `uuid`, `title`, `authors`, `progress`, `percentage`, `last_read`
+ - `highlights` (array): Text highlights
+ - `datetime` (string): Creation timestamp
+ - `text` (string): Highlighted text content
+ - `chapter` (integer): Chapter number
+ - `pos0` (string): EPUB CFI start
+ - `pos1` (string): EPUB CFI end
+ - `page_start` (integer): Start page
+ - `page_end` (integer): End page
+ - `color` (string, optional): Highlight color
+ - `notes` (array): Notes
+ - `datetime` (string): Creation timestamp
+ - `text` (string): Note content
+ - `chapter` (integer): Chapter number
+ - `pos0` (string, optional): Related position
+
+ **Response:**
+ - `progress_synced` (boolean): Progress sync status
+ - `highlights_synced` (integer): Highlights synced
+ - `notes_synced` (integer): Notes synced
+ - `conflicts_resolved` (integer): Conflict count
+ - `timestamp` (string): Sync timestamp
+
+ **KOReader Annotation Model:**
+ - **Separate arrays**: Highlights and notes stored separately
+ - **Linked**: Notes can reference highlights
+ - **Rich metadata**: Timestamps, positions, page numbers
+ - **Flexible**: Supports complex annotations
+
+ **Highlight Features:**
+ - Multi-color highlighting (via plugins)
+ - Precise text selection with EPUB CFI
+ - Page range tracking
+ - Chapter references
+ - Timestamps for sorting
+ - Full text preserved
+
+ **Note Features:**
+ - Free-form text notes
+ - Can be standalone or attached
+ - Chapter-based organization
+ - Timestamped
+ - Longer form than highlights
+ - Support for markdown (some versions)
+
+ **Color Support (via plugins):**
+ - Yellow: Default highlight
+ - Green: Important passages
+ - Blue: Key concepts
+ - Red: Critical information
+ - Orange: Interesting quotes
+ - Custom colors available
+
+ **Sync Advantages:**
+ - Separation allows granular control
+ - Highlights sync without notes
+ - Notes sync independently
+ - Better conflict resolution
+ - Efficient for large annotation sets
+
+ **Use Cases:**
+ - Academic research
+ - Study groups
+ - Content analysis
+ - Personal knowledge management
+ - Sharing insights
diff --git a/bruno-yaml/devices/koreader/scenarios/Sync Progress.yml b/bruno-yaml/devices/koreader/scenarios/Sync Progress.yml
new file mode 100644
index 0000000..138a404
--- /dev/null
+++ b/bruno-yaml/devices/koreader/scenarios/Sync Progress.yml
@@ -0,0 +1,53 @@
+info:
+ name: KOReader Sync Progress
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/sync/koreader/progress'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"library_id\": null,\n \"books\": [\n {\n \"\
+ uuid\": \"{{book_uuid"
+ headers:
+ - key: Authorization
+ value: Bearer {{device_token
+
+docs: |-
+ ## KOReader Sync Progress
+
+ Synchronizes reading progress from a KOReader device to the Bookhoard server.
+
+ **Method:** POST
+
+ **Endpoint:** /api/sync/koreader/progress
+
+ **Authentication:** Bearer token (device token)
+
+ **Request Body:**
+ - `library_id` (string, optional): Library UUID
+ - `books` (array): Array of book progress objects
+ - `uuid` (string): Book UUID
+ - `title` (string): Book title
+ - `authors` (array): List of authors
+ - `progress` (number): Progress value
+ - `percentage` (number): Percentage complete (0-1)
+ - `last_read` (string): ISO 8601 timestamp
+ - `chapter` (number): Current chapter
+ - `epubcfi` (string): EPUB Canonical Fragment Identifier
+ - `page` (number): Current page
+ - `total_pages` (number): Total pages
+ - `sync_mode` (string): Sync mode (immediate, deferred)
+ - `device_info` (object): Device information
+ - `koreader_version` (string): KOReader version
+ - `device_model` (string): Device model identifier
+
+ **Response:**
+ - `sync_status` (string): Sync status (accepted, processing)
+ - `books_synced` (number): Number of books synced
+
+ **Status Codes:**
+ - 202: Accepted - sync queued
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/scenarios/Add Books to Kobo Shelf.yml b/bruno-yaml/devices/scenarios/Add Books to Kobo Shelf.yml
new file mode 100644
index 0000000..3cb6cb4
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Add Books to Kobo Shelf.yml
@@ -0,0 +1,23 @@
+info:
+ name: Add Books to Kobo Shelf
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{baseURL}}/api/devices/{{deviceID}}/shelves'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"media_item_ids\": [\n \"{{bookUUID1"
+ headers:
+ - key: Authorization
+ value: Bearer {{userToken
+
+docs: |-
+ ## Add Books to Kobo Shelf
+
+ Add one or more books to a Kobo device shelf. Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization.
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/{deviceID
diff --git a/bruno-yaml/devices/scenarios/Approve Device Registration.yml b/bruno-yaml/devices/scenarios/Approve Device Registration.yml
new file mode 100644
index 0000000..ca6894a
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Approve Device Registration.yml
@@ -0,0 +1,40 @@
+info:
+ name: Approve Device Registration
+ type: http
+ seq: 6
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/approve/{{registration_id}}'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Approve Device Registration
+
+ Approves a pending device registration request.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/approve/:registration_id
+
+ **Authentication:** Bearer token
+
+ **Path Parameters:**
+ - `registration_id` (string): Registration request UUID
+
+ **Response:**
+ - Success message with approved device details
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Registration not found
+ - 400: Invalid registration status
+
+ **Example Response:**
+ ```json
+ {
+ "message": "Device registration approved",
+ "device_id": "uuid",
+ "device_name": "My Kobo"
diff --git a/bruno-yaml/devices/scenarios/Approve Registration - KOReader.yml b/bruno-yaml/devices/scenarios/Approve Registration - KOReader.yml
new file mode 100644
index 0000000..47d47ed
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Approve Registration - KOReader.yml
@@ -0,0 +1,55 @@
+info:
+ name: Approve Registration - KOReader
+ type: http
+ seq: 13
+
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/approve/reg-uuid-123'
+ auth: inherit
+
+docs: |-
+ ## Approve KOReader Device Registration
+
+ Approves a pending KOReader device registration.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/approve/:registration_id
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `registration_id` (string): Example: `reg-uuid-123`
+
+ **Response:**
+ - `registration_id` (string): Approved registration UUID
+ - `device_id` (string): Generated device UUID
+ - `device_name` (string): KOReader device name
+ - `device_type` (string): `koreader`
+ - `status` (string): `approved`
+ - `access_token` (string): Device access token
+ - `sync_endpoints` (object):
+ - `bookmarks` (string): Bookmarks sync endpoint
+ - `progress` (string): Progress sync endpoint
+ - `highlights` (string): Highlights sync endpoint
+ - `annotations` (string): Annotations sync endpoint
+
+ **Status Codes:**
+ - 200: Approved successfully
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 404: Registration not found
+
+ **KOReader-Specific Features:**
+ - Supports per-book SHA-256 based progress tracking
+ - Syncs highlights with color and notes
+ - Syncs bookmarks with locations and timestamps
+ - Supports dictionary annotations
+ - Can sync custom highlight colors
+
+ **After Approval:**
+ - KOReader device can immediately sync
+ - Device shows up in device list as "KOReader"
+ - Access token is stored in device settings
+ - Initial sync pulls down existing user data
diff --git a/bruno-yaml/devices/scenarios/Approve Registration - Kobo.yml b/bruno-yaml/devices/scenarios/Approve Registration - Kobo.yml
new file mode 100644
index 0000000..20a914c
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Approve Registration - Kobo.yml
@@ -0,0 +1,60 @@
+info:
+ name: Approve Registration - Kobo
+ type: http
+ seq: 14
+
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/approve/reg-uuid-456'
+ auth: inherit
+
+docs: |-
+ ## Approve Kobo Device Registration
+
+ Approves a pending Kobo e-reader device registration.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/approve/:registration_id
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `registration_id` (string): Example: `reg-uuid-456`
+
+ **Response:**
+ - `registration_id` (string): Approved registration UUID
+ - `device_id` (string): Generated device UUID
+ - `device_name` (string): Kobo device name
+ - `device_type` (string): `kobo`
+ - `status` (string): `approved`
+ - `access_token` (string): Device access token
+ - `sync_endpoints` (object):
+ - `bookmark_sync` (string): Bookmark sync URL
+ - `markup_sync` (string): Markup/highlight sync URL
+ - `metadata_sync` (string): Metadata sync URL
+ - `kobo_features` (object):
+ - `supports_shelves` (boolean): Kobo shelf support
+ - `supports_dictionary` (boolean): Dictionary annotation support
+ - `supports_statistics` (boolean): Reading statistics support
+
+ **Status Codes:**
+ - 200: Approved successfully
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 404: Registration not found
+
+ **Kobo-Specific Features:**
+ - Native Kobo sync protocol support
+ - Shelves/collections sync
+ - Reading statistics sync
+ - Dictionary lookups with annotations
+ - Book metadata sync
+ - Pocket articles integration
+
+ **After Approval:**
+ - Kobo device can use native sync feature
+ - Device appears in Nickel (Kobo UI)
+ - Sync runs automatically when connected
+ - Shelves sync with collections
+ - Reading progress syncs across devices
diff --git a/bruno-yaml/devices/scenarios/Check Pending Registration.yml b/bruno-yaml/devices/scenarios/Check Pending Registration.yml
new file mode 100644
index 0000000..5f519a3
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Check Pending Registration.yml
@@ -0,0 +1,52 @@
+info:
+ name: Check Pending Registration
+ type: http
+ seq: 4
+
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/register/status'
+ auth: none
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ registration_id: registration-uuid-here
+
+docs: |-
+ ## Check Registration Status
+
+ Checks the current status of a device registration request.
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/register/status
+
+ **Authentication:** None
+
+ **Request Body:**
+ - `registration_id` (string): UUID received from registration request
+
+ **Response:**
+ - `registration_id` (string): The registration UUID
+ - `status` (string): Current status
+ - `pending`: Awaiting user approval
+ - `approved`: Registration approved, device ready
+ - `rejected`: Registration rejected by user
+ - `expired`: Registration expired (not approved in time)
+ - `device_name` (string): Name of the device
+ - `device_type` (string): Type of device
+ - `created_at` (string): Registration timestamp
+ - `updated_at` (string): Last status update timestamp
+
+ **Status Codes:**
+ - 200: Status retrieved successfully
+ - 404: Registration ID not found
+ - 400: Invalid registration ID format
+
+ **Polling Recommendations:**
+ - Poll every 5-10 seconds while status is `pending`
+ - Stop polling when status changes to `approved`, `rejected`, or `expired`
+ - Use exponential backoff for mobile devices to save battery
diff --git a/bruno-yaml/devices/scenarios/Check Registration Status.yml b/bruno-yaml/devices/scenarios/Check Registration Status.yml
new file mode 100644
index 0000000..cf3b5d3
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Check Registration Status.yml
@@ -0,0 +1,11 @@
+info:
+ name: Check Registration Status
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/register/status'
+ auth: none
+ body:
+ type: json
+ jsonBody: "{\n \"registration_id\": \"{{registrationId"
diff --git a/bruno-yaml/devices/scenarios/Clear Kobo Shelf.yml b/bruno-yaml/devices/scenarios/Clear Kobo Shelf.yml
new file mode 100644
index 0000000..e71a127
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Clear Kobo Shelf.yml
@@ -0,0 +1,22 @@
+info:
+ name: Clear Kobo Shelf
+ type: http
+ seq: 1
+http:
+ method: DELETE
+ url: '{{baseURL}}/api/devices/{{deviceID}}/shelves/clear?shelf={{shelfName}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{userToken
+
+docs: |-
+ ## Clear Kobo Shelf
+
+ Clear all books from a Kobo device shelf, or all shelves if no shelf name is specified.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/devices/{deviceID
diff --git a/bruno-yaml/devices/scenarios/Create Device File Alias.yml b/bruno-yaml/devices/scenarios/Create Device File Alias.yml
new file mode 100644
index 0000000..5963f1d
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Create Device File Alias.yml
@@ -0,0 +1,55 @@
+info:
+ name: Create Device File Alias
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/{{device_id}}/file-aliases'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"file_path\": \"/mnt/sd/books/my-book.kepub.epub\",\n \"\
+ media_item_id\": \"{{media_item_id"
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_create_device_file_alias_success(status, headers, body) {\n if\
+ \ (status !== 201 && status !== 200) {\n throw new Error(\"Expected status\
+ \ 201 or 200, got \" + status);"
+
+docs: |-
+ ## Create Device File Alias
+
+ Creates a new file alias for a device. File aliases map device-specific file paths to media items.
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/:id/file-aliases
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `id` (string, required): Device UUID
+
+ **Request Body:**
+ - `file_path` (string, required): Device-specific file path
+ - `media_item_id` (string, required): Media item UUID to link to
+
+ **Response:** Created file alias object
+ - `id` (string): Alias UUID
+ - `device_id` (string): Device UUID
+ - `file_path` (string): Device-specific file path
+ - `media_item_id` (string): Associated media item UUID
+ - `created_at` (string): Creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 201: Created
+ - 200: Success
+ - 400: Invalid request body
+ - 401: Unauthorized
+ - 404: Device not found
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/scenarios/Delete Device.yml b/bruno-yaml/devices/scenarios/Delete Device.yml
new file mode 100644
index 0000000..f8e9671
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Delete Device.yml
@@ -0,0 +1,22 @@
+info:
+ name: Delete Device
+ type: http
+ seq: 6
+http:
+ method: DELETE
+ url: '{{base_url}}/api/devices/{{device_id}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{token
+
+docs: |-
+ ## Delete Device
+
+ Deletes a device and unregisters it from the user's account.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/devices/{deviceId
diff --git a/bruno-yaml/devices/scenarios/Disable Device Sync.yml b/bruno-yaml/devices/scenarios/Disable Device Sync.yml
new file mode 100644
index 0000000..ea34ca3
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Disable Device Sync.yml
@@ -0,0 +1,58 @@
+info:
+ name: Disable Device Sync
+ type: http
+ seq: 8
+
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/{{device_id}}'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ device_name: My Kobo Clara
+ sync_enabled: false
+ auto_sync: false
+ sync_frequency_minutes: 30
+
+docs: |-
+ ## Disable Device Sync
+
+ Disables synchronization for a specific device.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/devices/:device_id
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `device_id` (string): UUID of the device
+
+ **Request Body:**
+ - `device_name` (string, optional): Device name
+ - `sync_enabled` (boolean): Must be `false`
+ - `auto_sync` (boolean): Should be `false`
+ - `sync_frequency_minutes` (integer, optional): Any value (sync disabled)
+
+ **Response:**
+ - `id` (string): Device UUID
+ - `device_name` (string): Device name
+ - `sync_enabled` (boolean): `false`
+ - `auto_sync` (boolean): `false`
+ - `message` (string): Confirmation message
+
+ **Status Codes:**
+ - 200: Sync disabled successfully
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 404: Device not found
+
+ **Use Cases:**
+ - Temporarily disable sync for troubleshooting
+ - Stop sync for a lost or stolen device
+ - Disable sync before selling or giving away device
+ - Prevent data usage on limited connections
diff --git a/bruno-yaml/devices/scenarios/Get Device File Aliases.yml b/bruno-yaml/devices/scenarios/Get Device File Aliases.yml
new file mode 100644
index 0000000..9681aeb
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Get Device File Aliases.yml
@@ -0,0 +1,44 @@
+info:
+ name: Get Device File Aliases
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/{{device_id}}/file-aliases'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_get_device_file_aliases_success(status, headers, body) {\n if (status\
+ \ !== 200) {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## Get Device File Aliases
+
+ Retrieves all file aliases for a specific device. File aliases are used to map device-specific file paths to media items.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/:id/file-aliases
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `id` (string, required): Device UUID
+
+ **Response:** Array of file alias objects
+ - `id` (string): Alias UUID
+ - `device_id` (string): Device UUID
+ - `file_path` (string): Device-specific file path
+ - `media_item_id` (string): Associated media item UUID
+ - `created_at` (string): Creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Device not found
+ - 500: Internal server error
diff --git a/bruno-yaml/devices/scenarios/Get Device.yml b/bruno-yaml/devices/scenarios/Get Device.yml
new file mode 100644
index 0000000..e84f6c2
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Get Device.yml
@@ -0,0 +1,45 @@
+info:
+ name: Get Device Details
+ type: http
+ seq: 6
+
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/{{device_id}}'
+ auth: inherit
+
+docs: |-
+ ## Get Device Details
+
+ Retrieves detailed information about a specific device.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/:device_id
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `device_id` (string): UUID of the device
+
+ **Response:**
+ - `id` (string): Device UUID
+ - `device_name` (string): Device name
+ - `device_type` (string): `kobo`, `koreader`, or `web`
+ - `device_identifier` (string): Unique identifier
+ - `sync_enabled` (boolean): Sync status
+ - `auto_sync` (boolean): Auto-sync setting
+ - `sync_frequency_minutes` (integer): Sync interval
+ - `last_synced_at` (string): Last sync timestamp
+ - `sync_stats` (object): Sync statistics
+ - `total_syncs` (integer): Number of successful syncs
+ - `last_sync_status` (string): Status of last sync
+ - `bytes_synced` (integer): Total data transferred
+ - `created_at` (string): Registration timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Forbidden (device belongs to different user)
+ - 404: Device not found
diff --git a/bruno-yaml/devices/scenarios/Get Kobo Shelf.yml b/bruno-yaml/devices/scenarios/Get Kobo Shelf.yml
new file mode 100644
index 0000000..f7df037
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Get Kobo Shelf.yml
@@ -0,0 +1,22 @@
+info:
+ name: Get Kobo Shelf Books
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{baseURL}}/api/devices/{{deviceID}}/shelves?shelf={{shelfName}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{userToken
+
+docs: |-
+ ## Get Kobo Shelf Books
+
+ Get all books on a Kobo device shelf, optionally filter by shelf name.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/{deviceID
diff --git a/bruno-yaml/devices/scenarios/Initiate Device Registration.yml b/bruno-yaml/devices/scenarios/Initiate Device Registration.yml
new file mode 100644
index 0000000..c9aad41
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Initiate Device Registration.yml
@@ -0,0 +1,12 @@
+info:
+ name: Initiate Device Registration
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/register'
+ auth: none
+ body:
+ type: json
+ jsonBody: "{\n \"device_name\": \"My Kindle Paperwhite\",\n \"device_type\"\
+ : \"koreader\",\n \"device_identifier\": \"kindle-pw5-hardware-id-12345\""
diff --git a/bruno-yaml/devices/scenarios/List Devices.yml b/bruno-yaml/devices/scenarios/List Devices.yml
new file mode 100644
index 0000000..57c8712
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/List Devices.yml
@@ -0,0 +1,41 @@
+info:
+ name: List User Devices
+ type: http
+ seq: 5
+
+http:
+ method: GET
+ url: '{{base_url}}/api/devices'
+ auth: inherit
+
+docs: |-
+ ## List User Devices
+
+ Retrieves all registered devices for the authenticated user.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices
+
+ **Authentication:** Required (Bearer token)
+
+ **Response:**
+ - Array of device objects:
+ - `id` (string): Device UUID
+ - `device_name` (string): Human-readable device name
+ - `device_type` (string): `kobo`, `koreader`, or `web`
+ - `device_identifier` (string): Unique device identifier
+ - `sync_enabled` (boolean): Whether sync is active
+ - `auto_sync` (boolean): Whether automatic sync is enabled
+ - `sync_frequency_minutes` (integer): Sync interval in minutes
+ - `last_synced_at` (string): Last successful sync timestamp
+ - `created_at` (string): Registration timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Usage:**
+ - Display user's devices in account settings
+ - Allow users to manage sync settings per device
+ - Show last sync time for each device
diff --git a/bruno-yaml/devices/scenarios/List Pending Registrations.yml b/bruno-yaml/devices/scenarios/List Pending Registrations.yml
new file mode 100644
index 0000000..dc4a77d
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/List Pending Registrations.yml
@@ -0,0 +1,46 @@
+info:
+ name: Get Pending Registrations
+ type: http
+ seq: 11
+
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/pending'
+ auth: inherit
+
+docs: |-
+ ## Get Pending Registrations
+
+ Retrieves all pending device registration requests awaiting approval.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/pending
+
+ **Authentication:** Required (Bearer token)
+
+ **Response:**
+ - Array of pending registration objects:
+ - `registration_id` (string): Registration UUID
+ - `device_name` (string): Name of the device
+ - `device_type` (string): `kobo`, `koreader`, or `web`
+ - `device_identifier` (string): Device ID (may be masked)
+ - `created_at` (string): Request timestamp
+ - `expires_at` (string): Expiration timestamp
+ - `status` (string): Always `pending`
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Use Cases:**
+ - Show pending registrations in user settings
+ - Allow users to review devices before approval
+ - Display registration request details
+ - Provide approve/reject actions
+
+ **Security Notes:**
+ - Full device identifier may be partially masked
+ - Only shows registrations for authenticated user
+ - Expired registrations are automatically removed
+ - Users can only see their own pending registrations
diff --git a/bruno-yaml/devices/scenarios/Register Device.yml b/bruno-yaml/devices/scenarios/Register Device.yml
new file mode 100644
index 0000000..1a432d4
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Register Device.yml
@@ -0,0 +1,49 @@
+info:
+ name: Register Device
+ type: http
+ seq: 1
+
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/register'
+ auth: none
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ device_name: My Kobo Clara
+ device_type: kobo
+ device_identifier: N1234567890123
+
+docs: |-
+ ## Register Device
+
+ Initiates device registration by sending device information to the server.
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/register
+
+ **Authentication:** None (public endpoint)
+
+ **Request Body:**
+ - `device_name` (string): Human-readable name for the device
+ - `device_type` (string): Type of device - `kobo`, `koreader`, or `web`
+ - `device_identifier` (string): Unique device identifier (serial number or other ID)
+
+ **Response:**
+ - `registration_id` (string): UUID for tracking registration status
+ - `status` (string): Registration status - `pending`, `approved`, or `rejected`
+ - `message` (string): Status message
+
+ **Status Codes:**
+ - 201: Registration initiated successfully
+ - 400: Invalid request data
+ - 409: Device already registered
+
+ **Notes:**
+ - Device registration requires user approval before activation
+ - Registration ID should be stored for status checking
+ - Device identifier must be unique per device
diff --git a/bruno-yaml/devices/scenarios/Register KOReader Device.yml b/bruno-yaml/devices/scenarios/Register KOReader Device.yml
new file mode 100644
index 0000000..a4b087e
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Register KOReader Device.yml
@@ -0,0 +1,48 @@
+info:
+ name: Register KOReader Device
+ type: http
+ seq: 2
+
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/register'
+ auth: none
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ device_name: My Kindle Paperwhite
+ device_type: koreader
+ device_identifier: G090GP123456789
+
+docs: |-
+ ## Register KOReader Device
+
+ Registers a KOReader device (typically Kindle devices running KOReader software).
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/register
+
+ **Authentication:** None (public endpoint)
+
+ **Request Body:**
+ - `device_name` (string): Human-readable name for the device
+ - `device_type` (string): Must be `koreader`
+ - `device_identifier` (string): Kindle serial number or unique device ID
+
+ **Response:**
+ - `registration_id` (string): UUID for tracking registration status
+ - `status` (string): Registration status - `pending`
+ - `message` (string): Confirmation message
+
+ **Status Codes:**
+ - 201: Registration initiated
+ - 400: Invalid device type or data
+
+ **KOReader Notes:**
+ - KOReader runs on Kindle, Kobo, PocketBook, and Android devices
+ - Device identifier is typically the device serial number
+ - Supports bookmarks, progress, highlights, and annotations sync
diff --git a/bruno-yaml/devices/scenarios/Register Web Device.yml b/bruno-yaml/devices/scenarios/Register Web Device.yml
new file mode 100644
index 0000000..f2b85c9
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Register Web Device.yml
@@ -0,0 +1,49 @@
+info:
+ name: Register Web Device
+ type: http
+ seq: 3
+
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/register'
+ auth: none
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ device_name: Chrome Browser
+ device_type: web
+ device_identifier: web-client-abc123
+
+docs: |-
+ ## Register Web Device
+
+ Registers a web browser client for reading progress sync.
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/register
+
+ **Authentication:** None (public endpoint)
+
+ **Request Body:**
+ - `device_name` (string): Browser identification (e.g., "Chrome Browser")
+ - `device_type` (string): Must be `web`
+ - `device_identifier` (string): Unique browser/client identifier
+
+ **Response:**
+ - `registration_id` (string): UUID for tracking
+ - `status` (string): `pending` or `auto-approved`
+ - `message` (string): Status message
+
+ **Status Codes:**
+ - 201: Registration successful
+ - 400: Invalid request
+
+ **Web Device Notes:**
+ - Web devices use localStorage for device identification
+ - May be auto-approved without admin intervention
+ - Used for browser-based reading progress tracking
+ - Supports manual progress updates, highlights, and notes
diff --git a/bruno-yaml/devices/scenarios/Reject Device Registration.yml b/bruno-yaml/devices/scenarios/Reject Device Registration.yml
new file mode 100644
index 0000000..e82c4ea
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Reject Device Registration.yml
@@ -0,0 +1,38 @@
+info:
+ name: Reject Device Registration
+ type: http
+ seq: 7
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/reject/{{registration_id}}'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Reject Device Registration
+
+ Rejects a pending device registration request.
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/reject/:registration_id
+
+ **Authentication:** Bearer token
+
+ **Path Parameters:**
+ - `registration_id` (string): Registration request UUID
+
+ **Response:**
+ - Success message confirming rejection
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Registration not found
+ - 400: Invalid registration status
+
+ **Example Response:**
+ ```json
+ {
+ "message": "device registration rejected"
diff --git a/bruno-yaml/devices/scenarios/Reject Registration - KOReader.yml b/bruno-yaml/devices/scenarios/Reject Registration - KOReader.yml
new file mode 100644
index 0000000..94f734f
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Reject Registration - KOReader.yml
@@ -0,0 +1,56 @@
+info:
+ name: Reject Registration - KOReader
+ type: http
+ seq: 16
+
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/reject/reg-uuid-123'
+ auth: inherit
+
+docs: |-
+ ## Reject KOReader Registration
+
+ Rejects a pending KOReader device registration.
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/reject/:registration_id
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `registration_id` (string): Example: `reg-uuid-123`
+
+ **Request Body:**
+ Optional rejection reason:
+ ```json
+ {
+ "reason": "Device not recognized or user cancelled"
+ }
+ ```
+
+ **Response:**
+ - `registration_id` (string): Rejected registration UUID
+ - `device_name` (string): KOReader device name
+ - `device_type` (string): `koreader`
+ - `status` (string): `rejected`
+ - `rejected_at` (string): Rejection timestamp
+
+ **Status Codes:**
+ - 200: Rejected successfully
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 404: Registration not found
+
+ **KOReader Rejection Handling:**
+ - KOReader app displays rejection message
+ - User can initiate new registration
+ - Previous device identifier can be reused
+ - No data is lost on the device
+
+ **Common Reasons:**
+ - User didn't initiate registration
+ - Wrong device type selected
+ - Testing/sample registration
+ - Security concerns
diff --git a/bruno-yaml/devices/scenarios/Reject Registration - Kobo.yml b/bruno-yaml/devices/scenarios/Reject Registration - Kobo.yml
new file mode 100644
index 0000000..d35ae67
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Reject Registration - Kobo.yml
@@ -0,0 +1,64 @@
+info:
+ name: Reject Registration - Kobo
+ type: http
+ seq: 17
+
+http:
+ method: POST
+ url: '{{base_url}}/api/devices/reject/reg-uuid-456'
+ auth: inherit
+
+docs: |-
+ ## Reject Kobo Registration
+
+ Rejects a pending Kobo e-reader device registration.
+
+ **Method:** POST
+
+ **Endpoint:** /api/devices/reject/:registration_id
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `registration_id` (string): Example: `reg-uuid-456`
+
+ **Request Body:**
+ Optional rejection reason:
+ ```json
+ {
+ "reason": "Unrecognized device or user cancelled"
+ }
+ ```
+
+ **Response:**
+ - `registration_id` (string): Rejected registration UUID
+ - `device_name` (string): Kobo device name
+ - `device_type` (string): `kobo`
+ - `status` (string): `rejected`
+ - `rejected_at` (string): Rejection timestamp
+ - `message` (string): User-friendly message
+
+ **Status Codes:**
+ - 200: Rejected successfully
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 404: Registration not found
+
+ **Kobo Rejection Handling:**
+ - Kobo sync service shows rejection on device
+ - Device remains unregistered
+ - User must retry registration process
+ - Device serial number can be reused
+
+ **Common Reasons:**
+ - Unrecognized Kobo device
+ - User didn't initiate sync
+ - Wrong account selected on device
+ - Security precaution
+ - Device sold/given away (prevent old owner access)
+
+ **Kobo-Specific Notes:**
+ - Kobo devices display rejection in sync settings
+ - Device may need to be rebooted to clear pending state
+ - No data is removed from the device
+ - Re-registration requires going through Kobo's sync setup again
diff --git a/bruno-yaml/devices/scenarios/Remove Book from Kobo Shelf.yml b/bruno-yaml/devices/scenarios/Remove Book from Kobo Shelf.yml
new file mode 100644
index 0000000..e1b6795
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Remove Book from Kobo Shelf.yml
@@ -0,0 +1,22 @@
+info:
+ name: Remove Book from Kobo Shelf
+ type: http
+ seq: 1
+http:
+ method: DELETE
+ url: '{{baseURL}}/api/devices/{{deviceID}}/shelves?media_item_id={{bookUUID}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{userToken
+
+docs: |-
+ ## Remove Book from Kobo Shelf
+
+ Remove a specific book from a Kobo device shelf, preventing it from syncing to that device.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/devices/{deviceID
diff --git a/bruno-yaml/devices/scenarios/Update Device Settings.yml b/bruno-yaml/devices/scenarios/Update Device Settings.yml
new file mode 100644
index 0000000..6f52b96
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Update Device Settings.yml
@@ -0,0 +1,61 @@
+info:
+ name: Update Device Settings
+ type: http
+ seq: 7
+
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/{{device_id}}'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ device_name: Updated Device Name
+ sync_enabled: true
+ auto_sync: true
+ sync_frequency_minutes: 10
+
+docs: |-
+ ## Update Device Settings
+
+ Updates device configuration and sync settings.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/devices/:device_id
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `device_id` (string): UUID of the device
+
+ **Request Body:**
+ - `device_name` (string, optional): New device name
+ - `sync_enabled` (boolean, optional): Enable/disable sync
+ - `auto_sync` (boolean, optional): Enable automatic sync
+ - `sync_frequency_minutes` (integer, optional): Sync interval (5-1440 minutes)
+
+ **Response:**
+ - `id` (string): Device UUID
+ - `device_name` (string): Updated device name
+ - `sync_enabled` (boolean): Updated sync status
+ - `auto_sync` (boolean): Updated auto-sync setting
+ - `sync_frequency_minutes` (integer): Updated sync interval
+ - `updated_at` (string): Update timestamp
+
+ **Status Codes:**
+ - 200: Settings updated successfully
+ - 400: Invalid sync frequency or data
+ - 401: Unauthorized
+ - 403: Forbidden (device belongs to different user)
+ - 404: Device not found
+
+ **Sync Frequency Guidelines:**
+ - Mobile devices: 10-30 minutes
+ - E-readers on WiFi: 15-60 minutes
+ - Web clients: 5-15 minutes
+ - Minimum: 5 minutes
+ - Maximum: 1440 minutes (24 hours)
diff --git a/bruno-yaml/devices/scenarios/Update Device.yml b/bruno-yaml/devices/scenarios/Update Device.yml
new file mode 100644
index 0000000..d91b6f2
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Update Device.yml
@@ -0,0 +1,12 @@
+info:
+ name: Update Device
+ type: http
+ seq: 5
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/{{device_id}}'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"device_name\": \"My Updated Kindle\",\n \"sync_enabled\"\
+ : true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 10"
diff --git a/bruno-yaml/devices/scenarios/Update Sync Frequency.yml b/bruno-yaml/devices/scenarios/Update Sync Frequency.yml
new file mode 100644
index 0000000..53b88df
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/Update Sync Frequency.yml
@@ -0,0 +1,66 @@
+info:
+ name: Update Sync Frequency
+ type: http
+ seq: 9
+
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/{{device_id}}'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+ body:
+ type: json
+ json:
+ device_name: My Kobo Clara
+ sync_enabled: true
+ auto_sync: true
+ sync_frequency_minutes: 15
+
+docs: |-
+ ## Update Sync Frequency
+
+ Changes how often the device automatically syncs with the server.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/devices/:device_id
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `device_id` (string): UUID of the device
+
+ **Request Body:**
+ - `device_name` (string, optional): Device name
+ - `sync_enabled` (boolean): Must be `true` for auto-sync
+ - `auto_sync` (boolean): Must be `true`
+ - `sync_frequency_minutes` (integer): New sync interval (5-1440)
+
+ **Response:**
+ - `id` (string): Device UUID
+ - `sync_enabled` (boolean): `true`
+ - `auto_sync` (boolean): `true`
+ - `sync_frequency_minutes` (integer): Updated interval
+ - `next_sync_at` (string): Estimated next sync time
+ - `updated_at` (string): Update timestamp
+
+ **Status Codes:**
+ - 200: Frequency updated successfully
+ - 400: Invalid frequency value
+ - 401: Unauthorized
+ - 403: Forbidden
+ - 404: Device not found
+
+ **Recommended Frequencies:**
+ - **Active reading:** 5-10 minutes
+ - **Normal usage:** 15-30 minutes
+ - **Occasional reading:** 30-60 minutes
+ - **Battery saving:** 60+ minutes
+ - **WiFi only (3G):** 30-60 minutes
+
+ **Notes:**
+ - More frequent sync = more battery usage
+ - Sync only occurs when device is online
+ - Manual sync can be triggered anytime regardless of frequency
diff --git a/bruno-yaml/devices/scenarios/regenerate-token-forbidden.yml b/bruno-yaml/devices/scenarios/regenerate-token-forbidden.yml
new file mode 100644
index 0000000..bd1209a
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/regenerate-token-forbidden.yml
@@ -0,0 +1,24 @@
+info:
+ name: Regenerate Device Token - Forbidden
+ type: http
+ seq: 3
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/{{other_device_id}}/regenerate-token'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Regenerate Device Token - Forbidden
+
+ Tests that users cannot regenerate tokens for devices belonging to other users.
+
+ **Expected Behavior:** Returns 403 Forbidden when trying to regenerate token for another user's device
+
+ **Status Codes:**
+ - 403: Forbidden (device belongs to different user)
+
+ **Use Case:** Verify authorization - users can only manage their own devices
+
+ **Setup:** Use Bearer token from user A, try to regenerate token for user B's device
diff --git a/bruno-yaml/devices/scenarios/regenerate-token-notfound.yml b/bruno-yaml/devices/scenarios/regenerate-token-notfound.yml
new file mode 100644
index 0000000..3cf606f
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/regenerate-token-notfound.yml
@@ -0,0 +1,24 @@
+info:
+ name: Regenerate Device Token - Not Found
+ type: http
+ seq: 4
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/00000000-0000-0000-0000-000000000000/regenerate-token'
+ auth: bearer
+ body:
+ type: none
+
+docs: |-
+ ## Regenerate Device Token - Not Found
+
+ Tests that token regeneration returns 404 for non-existent devices.
+
+ **Expected Behavior:** Returns 404 Not Found when device UUID doesn't exist
+
+ **Status Codes:**
+ - 404: Device not found
+
+ **Use Case:** Verify proper error handling for invalid device IDs
+
+ **Setup:** Use all-zero UUID (guaranteed to not exist in database)
diff --git a/bruno-yaml/devices/scenarios/regenerate-token-unauthorized.yml b/bruno-yaml/devices/scenarios/regenerate-token-unauthorized.yml
new file mode 100644
index 0000000..5369ad1
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/regenerate-token-unauthorized.yml
@@ -0,0 +1,22 @@
+info:
+ name: Regenerate Device Token - Unauthorized
+ type: http
+ seq: 2
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/{{device_id}}/regenerate-token'
+ auth: none
+ body:
+ type: none
+
+docs: |-
+ ## Regenerate Device Token - Unauthorized
+
+ Tests that token regeneration requires authentication.
+
+ **Expected Behavior:** Returns 401 Unauthorized when no Bearer token is provided
+
+ **Status Codes:**
+ - 401: Unauthorized (missing or invalid token)
+
+ **Use Case:** Verify authentication is required for token regeneration
diff --git a/bruno-yaml/devices/scenarios/regenerate-token.yml b/bruno-yaml/devices/scenarios/regenerate-token.yml
new file mode 100644
index 0000000..dcff755
--- /dev/null
+++ b/bruno-yaml/devices/scenarios/regenerate-token.yml
@@ -0,0 +1,19 @@
+info:
+ name: Regenerate Device Token
+ type: http
+ seq: 1
+http:
+ method: PUT
+ url: '{{base_url}}/api/devices/{{device_id}}/regenerate-token'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Regenerate Device Token
+
+ Regenerates auth token for a device, invalidating old token immediately.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/devices/{device_id
diff --git a/bruno-yaml/environments/Bookhoard.yml b/bruno-yaml/environments/Bookhoard.yml
new file mode 100644
index 0000000..7d6a53c
--- /dev/null
+++ b/bruno-yaml/environments/Bookhoard.yml
@@ -0,0 +1,34 @@
+name: Bookhoard
+variables:
+ - name: base_url
+ value: http://localhost:8765
+ - name: media_item_id
+ value: 02a535a4-19f8-43fa-b81b-89a226d19dd9
+ - name: fake_book_id
+ value: 123e4567-e89b-12d3-a456-426614174000
+ - name: user_id
+ value: c51118f0-31fc-4c32-827d-517d6599bf21
+ - name: highlight_id
+ value: 660f9501-f29b-51d4-b716-446655440001
+ - name: note_id
+ value: 7710a602-g29b-61d4-c716-446655440002
+ - name: library_id
+ value: cc23c3a7-f8fb-451a-a78d-2a16df1b725a
+ - name: job_id
+ value: 550e8400-e29b-41d4-a716-446655440000
+ - name: rating
+ value: "5"
+ - name: is_visible
+ value: "true"
+ - name: library_folder
+ value: /app/uploads
+ - name: opds_base_url
+ value: ""
+ - secret: true
+ name: token
+ - secret: true
+ name: refresh_token
+ - secret: true
+ name: kobo_device_token
+ - secret: true
+ name: other_device_id
diff --git a/bruno-yaml/highlights/Create Media Highlight.yml b/bruno-yaml/highlights/Create Media Highlight.yml
new file mode 100644
index 0000000..b4abe89
--- /dev/null
+++ b/bruno-yaml/highlights/Create Media Highlight.yml
@@ -0,0 +1,41 @@
+info:
+ name: Create Media Highlight
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/highlights'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"selection_text\": \"This is the highlighted text from the\
+ \ media item.\",\n \"start_position\": \"page:45:offset:120\",\n \"end_position\"\
+ : \"page:45:offset:145\",\n \"color\": \"#ffff00\",\n \"note_id\": \"\""
+
+docs: |-
+ ## Create Media Highlight
+
+ Creates a new highlight for a specific media item.
+
+ **Method:** POST
+
+ **Endpoint:** /api/media-items/:id/highlights
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+
+ **Request Body:**
+ - `selection_text` (string): Highlighted text (required, 1-5000 chars)
+ - `start_position` (string): Start position (required, max 100 chars)
+ - `end_position` (string): End position (required, max 100 chars)
+ - `color` (string): Highlight color in hex format (optional, default #ffff00)
+ - `note_id` (string): Optional associated note ID
+
+ **Response:**
+ - Highlight object with all fields including generated ID and timestamps
+
+ **Status Codes:**
+ - 201: Created
+ - 400: Invalid request
+ - 401: Unauthorized
+ - 404: Media item not found
diff --git a/bruno-yaml/highlights/Delete Media Highlight.yml b/bruno-yaml/highlights/Delete Media Highlight.yml
new file mode 100644
index 0000000..45ffff4
--- /dev/null
+++ b/bruno-yaml/highlights/Delete Media Highlight.yml
@@ -0,0 +1,31 @@
+info:
+ name: Delete Media Highlight
+ type: http
+ seq: 5
+http:
+ method: DELETE
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Delete Media Highlight
+
+ Deletes a specific highlight.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/media-items/:id/highlights/:highlightId
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+ - `highlightId` (string): Highlight ID
+
+ **Response:**
+ - 204 No Content on success
+
+ **Status Codes:**
+ - 204: Success
+ - 401: Unauthorized
+ - 404: Highlight not found
diff --git a/bruno-yaml/highlights/Get Media Highlights.yml b/bruno-yaml/highlights/Get Media Highlights.yml
new file mode 100644
index 0000000..01bded4
--- /dev/null
+++ b/bruno-yaml/highlights/Get Media Highlights.yml
@@ -0,0 +1,40 @@
+info:
+ name: Get Media Highlights
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/highlights'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get Media Highlights
+
+ Retrieves all highlights for a specific media item for the authenticated user.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/:id/highlights
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+
+ **Response:**
+ - Array of highlight objects with fields:
+ - `id` (string): Highlight ID
+ - `media_item_id` (string): Media item ID
+ - `user_id` (string): User ID
+ - `selection_text` (string): Highlighted text
+ - `start_position` (string): Start position
+ - `end_position` (string): End position
+ - `color` (string): Highlight color (hex)
+ - `note_id` (string): Optional associated note ID
+ - `created_at` (string): Creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Media item not found
diff --git a/bruno-yaml/highlights/Get Single Media Highlight.yml b/bruno-yaml/highlights/Get Single Media Highlight.yml
new file mode 100644
index 0000000..8a12d99
--- /dev/null
+++ b/bruno-yaml/highlights/Get Single Media Highlight.yml
@@ -0,0 +1,31 @@
+info:
+ name: Get Single Media Highlight
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get Single Media Highlight
+
+ Retrieves a specific highlight by ID.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/:id/highlights/:highlightId
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+ - `highlightId` (string): Highlight ID
+
+ **Response:**
+ - Highlight object with all fields
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Highlight not found
diff --git a/bruno-yaml/highlights/Update Media Highlight.yml b/bruno-yaml/highlights/Update Media Highlight.yml
new file mode 100644
index 0000000..1324996
--- /dev/null
+++ b/bruno-yaml/highlights/Update Media Highlight.yml
@@ -0,0 +1,42 @@
+info:
+ name: Update Media Highlight
+ type: http
+ seq: 4
+http:
+ method: PUT
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"selection_text\": \"This is the updated highlighted text.\"\
+ ,\n \"start_position\": \"page:45:offset:125\",\n \"end_position\": \"\
+ page:45:offset:150\",\n \"color\": \"#ffeb3b\",\n \"note_id\": \"\""
+
+docs: |-
+ ## Update Media Highlight
+
+ Updates an existing highlight.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/media-items/:id/highlights/:highlightId
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+ - `highlightId` (string): Highlight ID
+
+ **Request Body:**
+ - `selection_text` (string): Updated highlighted text (required, 1-5000 chars)
+ - `start_position` (string): Updated start position (required, max 100 chars)
+ - `end_position` (string): Updated end position (required, max 100 chars)
+ - `color` (string): Updated highlight color in hex format (optional)
+ - `note_id` (string): Updated associated note ID (optional)
+
+ **Response:**
+ - Updated highlight object with all fields
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid request
+ - 401: Unauthorized
+ - 404: Highlight not found
diff --git a/bruno-yaml/library/Add Library Folder.yml b/bruno-yaml/library/Add Library Folder.yml
new file mode 100644
index 0000000..84ff051
--- /dev/null
+++ b/bruno-yaml/library/Add Library Folder.yml
@@ -0,0 +1,23 @@
+info:
+ name: Add Library Folder
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/libraries/{{library_id}}/folders'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"folder_path\": {{library_folder"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Add Library Folder
+
+ Adds a new folder path to a library for media scanning and indexing.
+
+ **Method:** POST
+
+ **Endpoint:** /api/libraries/{id
diff --git a/bruno-yaml/library/Create Library.yml b/bruno-yaml/library/Create Library.yml
new file mode 100644
index 0000000..02b0bbd
--- /dev/null
+++ b/bruno-yaml/library/Create Library.yml
@@ -0,0 +1,52 @@
+info:
+ name: Create Library
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/libraries'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"name\": \"My Ebook Library\",\n \"description\": \"A collection\
+ \ of technical books and novels\",\n \"type\": \"ebooks\""
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Create Library
+
+ Creates a new library for organizing media items.
+
+ **Method:** POST
+
+ **Endpoint:** /api/libraries
+
+ **Authentication:** Required (Bearer token)
+
+ **Request Body:**
+ - `name` (string, required): Library name
+ - `description` (string, optional): Library description
+ - `type` (string, required): Library type
+ - `"ebooks"`: Electronic books
+ - `"audiobooks"`: Audio books
+ - `"videos"`: Video content
+ - `"other"`: Other media types
+
+ **Response:** Library object
+ - `id` (string): Library UUID
+ - `name` (string): Library name
+ - `description` (string, optional): Library description
+ - `type` (string): Library type
+ - `is_visible` (boolean): Library visibility status
+ - `created_at` (string): Creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 201: Library created successfully
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 403: Forbidden (insufficient permissions)
+ - 409: Library name already exists
+ - 500: Internal server error
diff --git a/bruno-yaml/library/Delete Library Folder.yml b/bruno-yaml/library/Delete Library Folder.yml
new file mode 100644
index 0000000..b01d283
--- /dev/null
+++ b/bruno-yaml/library/Delete Library Folder.yml
@@ -0,0 +1,29 @@
+info:
+ name: Delete Library Folder
+ type: http
+ seq: 4
+http:
+ method: DELETE
+ url: '{{base_url}}/api/libraries/{{library_id}}/folders'
+ auth: inherit
+ body:
+ type: json
+ json:
+ folder_path: /path/to/folder
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_delete_library_folder_success(status, headers, body) {\n if (status\
+ \ !== 204) {\n throw new Error(\"Expected status 204, got \" + status);"
+
+docs: |-
+ ## Delete Library Folder
+
+ Removes a folder from a library's scanning configuration.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/libraries/{id
diff --git a/bruno-yaml/library/Delete Library.yml b/bruno-yaml/library/Delete Library.yml
new file mode 100644
index 0000000..822227e
--- /dev/null
+++ b/bruno-yaml/library/Delete Library.yml
@@ -0,0 +1,27 @@
+info:
+ name: Delete Library
+ type: http
+ seq: 3
+http:
+ method: DELETE
+ url: '{{base_url}}/api/libraries/{{library_id}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_delete_library_success(status, headers, body) {\n if (status !==\
+ \ 204) {\n throw new Error(\"Expected status 204, got \" + status);"
+
+docs: |-
+ ## Delete Library
+
+ Deletes a library and all associated media items.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/libraries/{id
diff --git a/bruno-yaml/library/Get Libraries (Admin).yml b/bruno-yaml/library/Get Libraries (Admin).yml
new file mode 100644
index 0000000..cad418a
--- /dev/null
+++ b/bruno-yaml/library/Get Libraries (Admin).yml
@@ -0,0 +1,41 @@
+info:
+ name: Get Libraries (Admin)
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/libraries'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Get Libraries (Admin)
+
+ Retrieves all libraries in the system (admin access required).
+
+ **Method:** GET
+
+ **Endpoint:** /api/libraries
+
+ **Authentication:** Required (Bearer token, admin permissions)
+
+ **Response:** Array of library objects
+ - `id` (string): Library UUID
+ - `name` (string): Library name
+ - `description` (string, optional): Library description
+ - `type` (string): Library type (ebooks, audiobooks, videos, other)
+ - `is_visible` (boolean): Library visibility to users
+ - `media_count` (number): Number of media items in library
+ - `folder_count` (number): Number of folders associated
+ - `created_at` (string): Creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Forbidden (admin access required)
+ - 500: Internal server error
diff --git a/bruno-yaml/library/Get Library Folders.yml b/bruno-yaml/library/Get Library Folders.yml
new file mode 100644
index 0000000..9127edc
--- /dev/null
+++ b/bruno-yaml/library/Get Library Folders.yml
@@ -0,0 +1,22 @@
+info:
+ name: Get Library Folders
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/libraries/{{library_id}}/folders'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Get Library Folders
+
+ Retrieves all folders associated with a specific library.
+
+ **Method:** GET
+
+ **Endpoint:** /api/libraries/{id
diff --git a/bruno-yaml/library/Get Library Stats.yml b/bruno-yaml/library/Get Library Stats.yml
new file mode 100644
index 0000000..ee4d131
--- /dev/null
+++ b/bruno-yaml/library/Get Library Stats.yml
@@ -0,0 +1,27 @@
+info:
+ name: Get Library Stats
+ type: http
+ seq: 5
+http:
+ method: GET
+ url: '{{base_url}}/api/libraries/{{library_id}}/stats'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_get_library_stats_success(status, headers, body) {\n if (status\
+ \ !== 200) {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## Get Library Stats
+
+ Retrieves statistical information about a library's media items.
+
+ **Method:** GET
+
+ **Endpoint:** /api/libraries/{id
diff --git a/bruno-yaml/library/Get Library Types.yml b/bruno-yaml/library/Get Library Types.yml
new file mode 100644
index 0000000..3f1abd5
--- /dev/null
+++ b/bruno-yaml/library/Get Library Types.yml
@@ -0,0 +1,34 @@
+info:
+ name: Get Library Types
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/libraries/types'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Get Library Types
+
+ Retrieves all available library types that can be used when creating libraries.
+
+ **Method:** GET
+
+ **Endpoint:** /api/libraries/types
+
+ **Authentication:** Required (Bearer token)
+
+ **Response:** Array of library type objects
+ - `id` (string): Type identifier (e.g., "ebooks", "audiobooks", "videos", "other")
+ - `name` (string): Display name for the type (e.g., "Ebooks", "Audiobooks", "Videos", "Other")
+ - `description` (string, optional): Description of what this type is used for
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/library/Get Library.yml b/bruno-yaml/library/Get Library.yml
new file mode 100644
index 0000000..d776a61
--- /dev/null
+++ b/bruno-yaml/library/Get Library.yml
@@ -0,0 +1,27 @@
+info:
+ name: Get Library
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/libraries/{{library_id}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_get_library_success(status, headers, body) {\n if (status !== 200)\
+ \ {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## Get Library
+
+ Retrieves detailed information about a specific library by ID.
+
+ **Method:** GET
+
+ **Endpoint:** /api/libraries/{id
diff --git a/bruno-yaml/library/Get Scan Settings.yml b/bruno-yaml/library/Get Scan Settings.yml
new file mode 100644
index 0000000..35d5a1f
--- /dev/null
+++ b/bruno-yaml/library/Get Scan Settings.yml
@@ -0,0 +1,37 @@
+info:
+ name: Get Scan Settings
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{base_url}}/api/library/scan-settings'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Get Scan Settings
+
+ Retrieves the user's current library scanning settings.
+
+ **Method:** GET
+
+ **Endpoint:** /api/library/scan-settings
+
+ **Authentication:** Required (Bearer token)
+
+ **Response:**
+ - `scan_frequency_minutes` (number): Minutes between automatic scans (minimum 1)
+ - `auto_scan_enabled` (boolean): Whether automatic scanning is enabled
+ - `last_scan_at` (string, optional): Timestamp of last scan
+ - `next_scan_at` (string, optional): Timestamp of next scheduled scan
+ - `library_id` (string, optional): Library ID for context
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Forbidden (access denied)
+ - 500: Internal server error
diff --git a/bruno-yaml/library/Get User Visible Libraries.yml b/bruno-yaml/library/Get User Visible Libraries.yml
new file mode 100644
index 0000000..fef95a4
--- /dev/null
+++ b/bruno-yaml/library/Get User Visible Libraries.yml
@@ -0,0 +1,38 @@
+info:
+ name: Get User Visible Libraries
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/libraries/visible'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Get User Visible Libraries
+
+ Retrieves all libraries that are visible to regular users.
+
+ **Method:** GET
+
+ **Endpoint:** /api/libraries/visible
+
+ **Authentication:** Required (Bearer token)
+
+ **Response:** Array of visible library objects
+ - `id` (string): Library UUID
+ - `name` (string): Library name
+ - `description` (string, optional): Library description
+ - `type` (string): Library type (ebooks, audiobooks, videos, other)
+ - `is_visible` (boolean): Always true for this endpoint
+ - `media_count` (number): Number of media items in library
+ - `created_at` (string): Creation timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 500: Internal server error
diff --git a/bruno-yaml/library/Set Library Visibility.yml b/bruno-yaml/library/Set Library Visibility.yml
new file mode 100644
index 0000000..587efe6
--- /dev/null
+++ b/bruno-yaml/library/Set Library Visibility.yml
@@ -0,0 +1,44 @@
+info:
+ name: Set Library Visibility
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/libraries/visibility'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"library_id\": \"{{library_id"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Set Library Visibility
+
+ Updates the visibility status of a library for regular users.
+
+ **Method:** POST
+
+ **Endpoint:** /api/libraries/visibility
+
+ **Authentication:** Required (Bearer token, admin permissions)
+
+ **Request Body:**
+ - `library_id` (string, required): Library UUID
+ - `is_visible` (boolean, required): Visibility status
+ - `true`: Library visible to all users
+ - `false`: Library hidden from regular users
+
+ **Response:** Updated library visibility object
+ - `library_id` (string): Library UUID
+ - `is_visible` (boolean): Updated visibility status
+ - `updated_at` (string): Update timestamp
+
+ **Status Codes:**
+ - 200: Visibility updated successfully
+ - 400: Invalid request data
+ - 401: Unauthorized
+ - 403: Forbidden (admin access required)
+ - 404: Library not found
+ - 500: Internal server error
diff --git a/bruno-yaml/library/Update Library.yml b/bruno-yaml/library/Update Library.yml
new file mode 100644
index 0000000..559278d
--- /dev/null
+++ b/bruno-yaml/library/Update Library.yml
@@ -0,0 +1,30 @@
+info:
+ name: Update Library
+ type: http
+ seq: 2
+http:
+ method: PUT
+ url: '{{base_url}}/api/libraries/{{library_id}}'
+ auth: inherit
+ body:
+ type: json
+ json:
+ name: Updated Library Name
+ description: Updated library description
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_update_library_success(status, headers, body) {\n if (status !==\
+ \ 200) {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## Update Library
+
+ Updates an existing library's information.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/libraries/{id
diff --git a/bruno-yaml/library/Update Scan Settings.yml b/bruno-yaml/library/Update Scan Settings.yml
new file mode 100644
index 0000000..f87a110
--- /dev/null
+++ b/bruno-yaml/library/Update Scan Settings.yml
@@ -0,0 +1,38 @@
+info:
+ name: Update Scan Settings
+ type: http
+ seq: 1
+http:
+ method: PUT
+ url: '{{base_url}}/api/library/scan-settings'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"scan_frequency_minutes\": 60,\n \"auto_scan_enabled\":\
+ \ true"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Update Scan Settings
+
+ Updates the user's media scanning settings.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/library/scan-settings
+
+ **Authentication:** Required
+
+ **Request Body:**
+ - `scan_frequency_minutes` (integer, required): Minutes between automatic scans (15-1440)
+ - `auto_scan_enabled` (boolean, required): Whether automatic scanning is enabled
+
+ **Response:**
+ - `message` (string): Success message
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid settings
+ - 401: Unauthorized
diff --git a/bruno-yaml/media-items/Create Media Item.yml b/bruno-yaml/media-items/Create Media Item.yml
new file mode 100644
index 0000000..100fc71
--- /dev/null
+++ b/bruno-yaml/media-items/Create Media Item.yml
@@ -0,0 +1,34 @@
+info:
+ name: Create Media Item
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/media-items'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: '"library_id": "{{library_id'
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_create_media_item_success(status, headers, body) {\n if (status\
+ \ !== 201) {\n throw new Error(\"Expected status 201, got \" + status);"
+
+docs: |-
+ ## Create Media Item
+
+ Creates a new media item in a library with full metadata.
+
+ **Method:** POST
+
+ **Endpoint:** /api/media-items
+
+ **Authentication:** Required (Bearer token, admin only)
+
+ **Prerequisites**
+ - Library must have at least one folder configured before media items can be added
+ - Use `POST /api/libraries/{library_id
diff --git a/bruno-yaml/media-items/Create Media Rating.yml b/bruno-yaml/media-items/Create Media Rating.yml
new file mode 100644
index 0000000..4ee95c8
--- /dev/null
+++ b/bruno-yaml/media-items/Create Media Rating.yml
@@ -0,0 +1,23 @@
+info:
+ name: Create Media Rating
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/rating'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"rating\": 8"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Create Media Rating
+
+ Creates or updates the authenticated user's rating for a specific media item.
+
+ **Method:** POST
+
+ **Endpoint:** /api/media-items/{id
diff --git a/bruno-yaml/media-items/Delete Media Item.yml b/bruno-yaml/media-items/Delete Media Item.yml
new file mode 100644
index 0000000..3c7af35
--- /dev/null
+++ b/bruno-yaml/media-items/Delete Media Item.yml
@@ -0,0 +1,27 @@
+info:
+ name: Delete Media Item
+ type: http
+ seq: 3
+http:
+ method: DELETE
+ url: '{{base_url}}/api/media-items/{{media_item_id}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_delete_media_item_success(status, headers, body) {\n if (status\
+ \ !== 204) {\n throw new Error(\"Expected status 204, got \" + status);"
+
+docs: |-
+ ## Delete Media Item
+
+ Deletes a media item from the library.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/media-items/{id
diff --git a/bruno-yaml/media-items/Delete Media Rating.yml b/bruno-yaml/media-items/Delete Media Rating.yml
new file mode 100644
index 0000000..ec1155d
--- /dev/null
+++ b/bruno-yaml/media-items/Delete Media Rating.yml
@@ -0,0 +1,27 @@
+info:
+ name: Delete Media Rating
+ type: http
+ seq: 5
+http:
+ method: DELETE
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/rating'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_delete_media_rating_success(status, headers, body) {\n if (status\
+ \ !== 204) {\n throw new Error(\"Expected status 204, got \" + status);"
+
+docs: |-
+ ## Delete Media Rating
+
+ Deletes a user's rating for a specific media item.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/media-items/{id
diff --git a/bruno-yaml/media-items/EPUB Download.yml b/bruno-yaml/media-items/EPUB Download.yml
new file mode 100644
index 0000000..ee399d6
--- /dev/null
+++ b/bruno-yaml/media-items/EPUB Download.yml
@@ -0,0 +1,19 @@
+info:
+ name: Download Media Item
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{baseURL}}/api/media-items/{{bookUUID}}/download'
+ auth: none
+ body:
+ type: none
+
+docs: |-
+ ## Download Media Item
+
+ Download a media item file (EPUB, PDF, etc.) from Bookhoard server.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/{bookUUID
diff --git a/bruno-yaml/media-items/Get Media Item.yml b/bruno-yaml/media-items/Get Media Item.yml
new file mode 100644
index 0000000..1cc75fb
--- /dev/null
+++ b/bruno-yaml/media-items/Get Media Item.yml
@@ -0,0 +1,25 @@
+info:
+ name: Get Media Item
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/{{media_item_id}}'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_get_media_item_success(status, headers, body) {\n if (status !==\
+ \ 200) {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## Get Media Item
+
+ Retrieves detailed information about a specific media item.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/{id
diff --git a/bruno-yaml/media-items/Get Media Rating.yml b/bruno-yaml/media-items/Get Media Rating.yml
new file mode 100644
index 0000000..207641c
--- /dev/null
+++ b/bruno-yaml/media-items/Get Media Rating.yml
@@ -0,0 +1,27 @@
+info:
+ name: Get Media Rating
+ type: http
+ seq: 4
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/rating'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_get_media_rating_success(status, headers, body) {\n if (status\
+ \ !== 200) {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## Get Media Rating
+
+ Retrieves a user's rating for a specific media item.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/{id
diff --git a/bruno-yaml/media-items/Update Media Item.yml b/bruno-yaml/media-items/Update Media Item.yml
new file mode 100644
index 0000000..3abcccb
--- /dev/null
+++ b/bruno-yaml/media-items/Update Media Item.yml
@@ -0,0 +1,52 @@
+info:
+ name: Update Media Item
+ type: http
+ seq: 2
+http:
+ method: PUT
+ url: '{{base_url}}/api/media-items/{{media_item_id}}'
+ auth: inherit
+ body:
+ type: json
+ json:
+ title: Updated Media Item Title
+ author: Updated Author Name
+ isbn: 978-9876543210
+ description: Updated description
+ cover_image_path: /updated/path/to/cover.jpg
+ series: Updated Series Name
+ series_number: 2
+ tags:
+ - updated
+ - fiction
+ - adventure
+ asin: B09XYZ789
+ date_published: '2023-02-20'
+ publisher: Updated Publisher
+ contributors:
+ - Updated Contributor
+ language: en
+ edition: Updated Edition
+ page_count: 400
+ genre: Updated Genre
+ copyright_year: 2023
+ goodreads_id: '7890123'
+ openlibrary_id: OL789012M
+ google_books_id: GB789012
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_update_media_item_success(status, headers, body) {\n if (status\
+ \ !== 200) {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## Update Media Item
+
+ Updates an existing media item's metadata.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/media-items/{id
diff --git a/bruno-yaml/media-items/Update Media Rating.yml b/bruno-yaml/media-items/Update Media Rating.yml
new file mode 100644
index 0000000..50f7725
--- /dev/null
+++ b/bruno-yaml/media-items/Update Media Rating.yml
@@ -0,0 +1,53 @@
+info:
+ name: Update Media Rating
+ type: http
+ seq: 1
+http:
+ method: PUT
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/rating'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"rating\": 4,\n \"review\": \"Great book! Very enjoyable\
+ \ read.\""
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_update_media_rating_success(status, headers, body) {\n if (status\
+ \ !== 200) {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## Update Media Rating
+
+ Updates an existing rating for a media item.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/media-items/:id/rating
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `id` (string, required): Media item UUID
+
+ **Request Body:**
+ - `rating` (number, required): Rating value (typically 1-5)
+ - `review` (string, optional): Review text
+
+ **Response:** Updated rating object
+ - `id` (string): Rating ID
+ - `media_item_id` (string): Media item UUID
+ - `rating` (number): Rating value
+ - `review` (string): Review text
+ - `created_at` (string): Creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid request body
+ - 401: Unauthorized
+ - 404: Media item or rating not found
+ - 500: Internal server error
diff --git a/bruno-yaml/media-items/scenarios/Filter Media Items.yml b/bruno-yaml/media-items/scenarios/Filter Media Items.yml
new file mode 100644
index 0000000..de398cd
--- /dev/null
+++ b/bruno-yaml/media-items/scenarios/Filter Media Items.yml
@@ -0,0 +1,58 @@
+info:
+ name: Filter Media Items
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/filtered?library_id={{library_id}}&genre_filter=Fiction&language_filter=en&year_min=2000&year_max=2024&limit=10&offset=0'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Filter Media Items
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/filtered
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `library_id` (string, required): UUID of the library
+ - `author_filter` (string, optional): Filter by author (partial match)
+ - `series_filter` (string, optional): Filter by series (partial match)
+ - `genre_filter` (string, optional): Filter by genre (exact match)
+ - `language_filter` (string, optional): Filter by language (exact match, e.g., 'en', 'es', 'fr')
+ - `year_min` (integer, optional): Minimum copyright year
+ - `year_max` (integer, optional): Maximum copyright year
+ - `has_cover` (boolean, optional): Filter for items with cover images only
+ - `sort` (string, optional): Sort field and direction (same options as ListMediaItems)
+ - `limit` (integer, optional): Number of items to return (default: 50, max: 1000)
+ - `offset` (integer, optional): Number of items to skip (default: 0)
+
+ **Response:** Object containing array of filtered media items
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Bad request (invalid parameters)
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Examples:**
+ - Filter by genre: `/api/media-items/filtered?library_id=xxx&genre_filter=Fiction`
+ - Filter by language: `/api/media-items/filtered?library_id=xxx&language_filter=es`
+ - Filter by year range: `/api/media-items/filtered?library_id=xxx&year_min=2000&year_max=2024`
+ - Filter by cover: `/api/media-items/filtered?library_id=xxx&has_cover=true`
+ - Combine filters: `/api/media-items/filtered?library_id=xxx&genre_filter=Sci-Fi&year_min=2010&language_filter=en`
+
+ **Filter Behavior:**
+ - Multiple filters can be combined (AND logic)
+ - Author and series filters use partial matching (ILIKE)
+ - Genre and language filters use exact matching
+ - Year range filters are inclusive
+ - Filters are applied before sorting and pagination
+ - User library visibility is respected
diff --git a/bruno-yaml/media-items/scenarios/List Media Items Sorted.yml b/bruno-yaml/media-items/scenarios/List Media Items Sorted.yml
new file mode 100644
index 0000000..3effa76
--- /dev/null
+++ b/bruno-yaml/media-items/scenarios/List Media Items Sorted.yml
@@ -0,0 +1,63 @@
+info:
+ name: List Media Items with Sorting
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items?library_id={{library_id}}&sort=title+ASC&limit=10&offset=0'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## List Media Items with Sorting
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `library_id` (string, required): UUID of the library
+ - `sort` (string, optional): Sort field and direction
+ - Available options:
+ - `created_at ASC` - Oldest added first
+ - `created_at DESC` - Newest added first (default)
+ - `title ASC` - Title A-Z
+ - `title DESC` - Title Z-A
+ - `author ASC` - Author A-Z
+ - `author DESC` - Author Z-A
+ - `series ASC` - Series order
+ - `series DESC` - Series reverse order
+ - `date_published ASC` - Oldest published first
+ - `date_published DESC` - Newest published first
+ - `copyright_year ASC` - Oldest copyright first
+ - `copyright_year DESC` - Newest copyright first
+ - `page_count ASC` - Shortest first
+ - `page_count DESC` - Longest first
+ - `genre ASC` - Genre A-Z
+ - `genre DESC` - Genre Z-A
+ - `limit` (integer, optional): Number of items to return (default: 50, max: 1000)
+ - `offset` (integer, optional): Number of items to skip (default: 0)
+
+ **Response:** Object containing array of media items
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Bad request (invalid parameters)
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Examples:**
+ - Sort by title: `/api/media-items?library_id=xxx&sort=title+ASC`
+ - Sort by author descending: `/api/media-items?library_id=xxx&sort=author+DESC`
+ - Sort by page count: `/api/media-items?library_id=xxx&sort=page_count+ASC`
+
+ **Sorting Behavior:**
+ - All sorts are secondary-sorted by series_number then title for consistency
+ - NULL values are sorted last for ascending, first for descending
+ - Sorting is case-insensitive for text fields
diff --git a/bruno-yaml/media-items/scenarios/List Media Items.yml b/bruno-yaml/media-items/scenarios/List Media Items.yml
new file mode 100644
index 0000000..6175989
--- /dev/null
+++ b/bruno-yaml/media-items/scenarios/List Media Items.yml
@@ -0,0 +1,49 @@
+info:
+ name: List Media Items
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items?library_id={{library_id}}&limit=20&offset=0'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_list_media_items_success(status, headers, body) {\n if (status\
+ \ !== 200) {\n throw new Error(\"Expected status 200, got \" + status);"
+
+docs: |-
+ ## List Media Items
+
+ Retrieves a paginated list of media items from a specific library.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `library_id` (string, required): Library UUID to filter items
+ - `limit` (number, optional): Number of results per page (default: 20, max: 100)
+ - `offset` (number, optional): Pagination offset (default: 0)
+
+ **Response:** Array of media item objects
+ - `id` (string): Media item UUID
+ - `title` (string): Media item title
+ - `library_id` (string): Library UUID
+ - `media_type` (string): Type of media (e.g., "ebook", "audiobook")
+ - `file_path` (string): Path to media file
+ - `created_at` (string): Creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid query parameters
+ - 401: Unauthorized
+ - 403: Forbidden (library access denied)
+ - 404: Library not found
+ - 500: Internal server error
diff --git a/bruno-yaml/media-items/scenarios/Search Media Items.yml b/bruno-yaml/media-items/scenarios/Search Media Items.yml
new file mode 100644
index 0000000..542724b
--- /dev/null
+++ b/bruno-yaml/media-items/scenarios/Search Media Items.yml
@@ -0,0 +1,62 @@
+info:
+ name: Search Media Items
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/search?q=harry'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_search_media_items_success(status, headers, body) {\n if (status\
+ \ !== 200 && status !== 404) {\n throw new Error(\"Expected status 200\
+ \ or 404, got \" + status);"
+
+docs: |-
+ ## Search Media Items
+
+ Performs a search across all visible media items using partial matching with fuzzy fallback.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/search
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `q` (string, required): Search query (minimum 2 characters)
+
+ **Search Behavior:**
+ 1. First performs case-insensitive partial matching across:
+ - Title
+ - Author
+ - Series
+ - Tags
+ - Contributors
+ 2. If no results found, falls back to fuzzy search using word_similarity with 0.3 threshold
+
+ **Response:** Array of media item objects (same structure as List Media Items)
+
+ **Status Codes:**
+ - 200: Success (results found)
+ - 404: No results found
+ - 400: Missing or invalid query parameter
+ - 401: Unauthorized
+ - 500: Internal server error
+
+ **Examples:**
+ - Search by title: `q=harry potter`
+ - Search by author: `q=king`
+ - Fuzzy search: `q=hary poter` (will find "harry potter")
+
+ **Ranking:**
+ Results are ranked by relevance:
+ - Title matches: Highest priority
+ - Author matches: High priority
+ - Series matches: Medium priority
+ - Tag matches: Lower priority
+ - Fuzzy matches: Sorted by similarity score
diff --git a/bruno-yaml/notes/Create Media Note.yml b/bruno-yaml/notes/Create Media Note.yml
new file mode 100644
index 0000000..1713978
--- /dev/null
+++ b/bruno-yaml/notes/Create Media Note.yml
@@ -0,0 +1,37 @@
+info:
+ name: Create Media Note
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/notes'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"content\": \"This is a test note about this media item.\"\
+ ,\n \"position\": \"page:45\""
+
+docs: |-
+ ## Create Media Note
+
+ Creates a new note for a specific media item.
+
+ **Method:** POST
+
+ **Endpoint:** /api/media-items/:id/notes
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+
+ **Request Body:**
+ - `content` (string): Note content (required, 1-10000 chars)
+ - `position` (string): Optional position reference (max 100 chars)
+
+ **Response:**
+ - Note object with all fields including generated ID and timestamps
+
+ **Status Codes:**
+ - 201: Created
+ - 400: Invalid request
+ - 401: Unauthorized
+ - 404: Media item not found
diff --git a/bruno-yaml/notes/Delete Media Note.yml b/bruno-yaml/notes/Delete Media Note.yml
new file mode 100644
index 0000000..2c08843
--- /dev/null
+++ b/bruno-yaml/notes/Delete Media Note.yml
@@ -0,0 +1,31 @@
+info:
+ name: Delete Media Note
+ type: http
+ seq: 5
+http:
+ method: DELETE
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/notes/{{note_id}}'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Delete Media Note
+
+ Deletes a specific note.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/media-items/:id/notes/:noteId
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+ - `noteId` (string): Note ID
+
+ **Response:**
+ - 204 No Content on success
+
+ **Status Codes:**
+ - 204: Success
+ - 401: Unauthorized
+ - 404: Note not found
diff --git a/bruno-yaml/notes/Get Media Notes.yml b/bruno-yaml/notes/Get Media Notes.yml
new file mode 100644
index 0000000..2be1688
--- /dev/null
+++ b/bruno-yaml/notes/Get Media Notes.yml
@@ -0,0 +1,37 @@
+info:
+ name: Get Media Notes
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/notes'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get Media Notes
+
+ Retrieves all notes for a specific media item for the authenticated user.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/:id/notes
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+
+ **Response:**
+ - Array of note objects with fields:
+ - `id` (string): Note ID
+ - `media_item_id` (string): Media item ID
+ - `user_id` (string): User ID
+ - `content` (string): Note content
+ - `position` (string): Optional position reference
+ - `created_at` (string): Creation timestamp
+ - `updated_at` (string): Last update timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Media item not found
diff --git a/bruno-yaml/notes/Get Single Media Note.yml b/bruno-yaml/notes/Get Single Media Note.yml
new file mode 100644
index 0000000..5e5dce6
--- /dev/null
+++ b/bruno-yaml/notes/Get Single Media Note.yml
@@ -0,0 +1,31 @@
+info:
+ name: Get Single Media Note
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/notes/{{note_id}}'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get Single Media Note
+
+ Retrieves a specific note by ID.
+
+ **Method:** GET
+
+ **Endpoint:** /api/media-items/:id/notes/:noteId
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+ - `noteId` (string): Note ID
+
+ **Response:**
+ - Note object with all fields
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Note not found
diff --git a/bruno-yaml/notes/Update Media Note.yml b/bruno-yaml/notes/Update Media Note.yml
new file mode 100644
index 0000000..6e894cc
--- /dev/null
+++ b/bruno-yaml/notes/Update Media Note.yml
@@ -0,0 +1,38 @@
+info:
+ name: Update Media Note
+ type: http
+ seq: 4
+http:
+ method: PUT
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/notes/{{note_id}}'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"content\": \"This is the updated note content.\",\n \"\
+ position\": \"page:47\""
+
+docs: |-
+ ## Update Media Note
+
+ Updates an existing note.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/media-items/:id/notes/:noteId
+
+ **Path Parameters:**
+ - `id` (string): Media item ID
+ - `noteId` (string): Note ID
+
+ **Request Body:**
+ - `content` (string): Updated note content (required, 1-10000 chars)
+ - `position` (string): Updated position reference (optional, max 100 chars)
+
+ **Response:**
+ - Updated note object with all fields
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid request
+ - 401: Unauthorized
+ - 404: Note not found
diff --git a/bruno-yaml/opds/Download Book EPUB.yml b/bruno-yaml/opds/Download Book EPUB.yml
new file mode 100644
index 0000000..b955b8e
--- /dev/null
+++ b/bruno-yaml/opds/Download Book EPUB.yml
@@ -0,0 +1,7 @@
+info:
+ name: Download Book (EPUB)
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{opds_base_url}}/devices/{{device_id}}/download/{{book_id}}'
diff --git a/bruno-yaml/opds/Download Book KEPUB (On-the-fly Conversion).yml b/bruno-yaml/opds/Download Book KEPUB (On-the-fly Conversion).yml
new file mode 100644
index 0000000..e92bf12
--- /dev/null
+++ b/bruno-yaml/opds/Download Book KEPUB (On-the-fly Conversion).yml
@@ -0,0 +1,19 @@
+info:
+ name: Download Book KEPUB (On-the-fly Conversion)
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/opds/devices/{{deviceId}}/download/{{mediaItemId}}?format=kepub'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Download Book KEPUB (On-the-fly Conversion)
+
+ Downloads a book in Kobo EPUB (KEPUB) format with on-the-fly conversion if needed.
+
+ **Method:** GET
+
+ **Endpoint:** /opds/devices/{deviceId
diff --git a/bruno-yaml/opds/Download Book KEPUB.yml b/bruno-yaml/opds/Download Book KEPUB.yml
new file mode 100644
index 0000000..f3f12ad
--- /dev/null
+++ b/bruno-yaml/opds/Download Book KEPUB.yml
@@ -0,0 +1,7 @@
+info:
+ name: Download Book (KEPUB)
+ type: http
+ seq: 4
+http:
+ method: GET
+ url: '{{opds_base_url}}/devices/{{device_id}}/download/{{book_id}}?format=kepub'
diff --git a/bruno-yaml/opds/Get Cover Image.yml b/bruno-yaml/opds/Get Cover Image.yml
new file mode 100644
index 0000000..1af1e22
--- /dev/null
+++ b/bruno-yaml/opds/Get Cover Image.yml
@@ -0,0 +1,7 @@
+info:
+ name: Get Cover Image
+ type: http
+ seq: 5
+http:
+ method: GET
+ url: '{{opds_base_url}}/devices/{{device_id}}/cover/{{book_id}}'
diff --git a/bruno-yaml/opds/Get Device Catalog.yml b/bruno-yaml/opds/Get Device Catalog.yml
new file mode 100644
index 0000000..81030c0
--- /dev/null
+++ b/bruno-yaml/opds/Get Device Catalog.yml
@@ -0,0 +1,7 @@
+info:
+ name: Get Device Catalog
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{opds_base_url}}/devices/{{device_id}}/catalog?page=1&per_page=50'
diff --git a/bruno-yaml/opds/Get Device Navigation.yml b/bruno-yaml/opds/Get Device Navigation.yml
new file mode 100644
index 0000000..0651861
--- /dev/null
+++ b/bruno-yaml/opds/Get Device Navigation.yml
@@ -0,0 +1,7 @@
+info:
+ name: Get Device Navigation
+ type: http
+ seq: 6
+http:
+ method: GET
+ url: '{{opds_base_url}}/devices/{{device_id}}/nav'
diff --git a/bruno-yaml/opds/List Formats.yml b/bruno-yaml/opds/List Formats.yml
new file mode 100644
index 0000000..612193e
--- /dev/null
+++ b/bruno-yaml/opds/List Formats.yml
@@ -0,0 +1,7 @@
+info:
+ name: List Formats
+ type: http
+ seq: 7
+http:
+ method: GET
+ url: '{{opds_base_url}}/devices/{{device_id}}/formats/{{book_id}}'
diff --git a/bruno-yaml/opds/Search Device Catalog.yml b/bruno-yaml/opds/Search Device Catalog.yml
new file mode 100644
index 0000000..d6d3d05
--- /dev/null
+++ b/bruno-yaml/opds/Search Device Catalog.yml
@@ -0,0 +1,7 @@
+info:
+ name: Search Device Catalog
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{opds_base_url}}/devices/{{device_id}}/search?q=hobbit'
diff --git a/bruno-yaml/opds/scenarios/Download Book - Query Token.yml b/bruno-yaml/opds/scenarios/Download Book - Query Token.yml
new file mode 100644
index 0000000..00e77e8
--- /dev/null
+++ b/bruno-yaml/opds/scenarios/Download Book - Query Token.yml
@@ -0,0 +1,16 @@
+info:
+ name: Download Book - Query Token
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{opds_base_url}}/opds/devices/{{device_id}}/download/{{book_id}}?token={{device_token}}'
+
+docs: |-
+ ## Download Book - Query Token
+
+ Tests OPDS book download using query parameter authentication.
+
+ **Method:** GET
+
+ **Endpoint:** /opds/devices/{device_id
diff --git a/bruno-yaml/opds/scenarios/Get Device Catalog - Bearer.yml b/bruno-yaml/opds/scenarios/Get Device Catalog - Bearer.yml
new file mode 100644
index 0000000..9477e9e
--- /dev/null
+++ b/bruno-yaml/opds/scenarios/Get Device Catalog - Bearer.yml
@@ -0,0 +1,16 @@
+info:
+ name: Get Device Catalog - Bearer
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{opds_base_url}}/opds/devices/{{device_id}}/catalog'
+
+docs: |-
+ ## Get Device OPDS Catalog - Bearer
+
+ Tests OPDS device catalog retrieval using Bearer token authentication.
+
+ **Method:** GET
+
+ **Endpoint:** /opds/devices/{device_id
diff --git a/bruno-yaml/opds/scenarios/Get Device Catalog - Query Token.yml b/bruno-yaml/opds/scenarios/Get Device Catalog - Query Token.yml
new file mode 100644
index 0000000..5113ea9
--- /dev/null
+++ b/bruno-yaml/opds/scenarios/Get Device Catalog - Query Token.yml
@@ -0,0 +1,16 @@
+info:
+ name: Get Device Catalog - Query Token
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{opds_base_url}}/opds/devices/{{device_id}}/catalog?token={{device_token}}'
+
+docs: |-
+ ## Get Device OPDS Catalog - Query Token
+
+ Tests OPDS device catalog retrieval using query parameter authentication.
+
+ **Method:** GET
+
+ **Endpoint:** /opds/devices/{device_id
diff --git a/bruno-yaml/opencollection.yml b/bruno-yaml/opencollection.yml
new file mode 100644
index 0000000..56977b2
--- /dev/null
+++ b/bruno-yaml/opencollection.yml
@@ -0,0 +1,203 @@
+opencollection: 1.0.0
+
+info:
+ name: Untitled Collection
+config:
+ proxy:
+ inherit: true
+ config:
+ protocol: http
+ hostname: ""
+ port: ""
+ auth:
+ username: ""
+ password: ""
+ bypassProxy: ""
+
+request:
+ auth:
+ type: bearer
+ token: "{{token}}"
+
+docs:
+ content: |-
+ # Bruno API Tests for Bookhoard
+ This directory contains Bruno collection for testing the Bookhoard API with comprehensive REST documentation.
+ Setup
+ 1. Install Bruno: https://www.usebruno.com/
+ 2. Open Bruno and import this collection folder
+ 3. Select the "localhost" environment
+ 4. Start the application with `podman-compose up --build` or `docker-compose up --build`
+ 5. Register/Login first, then use Bearer token for protected endpoints
+ Available Tests
+ Authentication (Public & Private)
+ - **Register User**: POST /api/auth/register - Create new account with role-based restrictions
+ - **Login User**: POST /api/auth/login - Authenticate (email or username)
+ - **Refresh Token**: POST /api/auth/refresh - Get new access token
+ - **Logout**: POST /api/auth/logout - Invalidate refresh token
+ User Profile Management
+ - **Get Profile**: GET /api/auth/profile - Get current user info
+ - **Update Profile**: PUT /api/auth/profile - Update first_name, last_name
+ - **Update Email**: PUT /api/auth/email - Update email address
+ - **Update Username**: PUT /api/auth/username - Update username
+ - **Update Password**: PUT /api/auth/password - Update password
+ - **Update Theme**: PUT /api/auth/theme - Update theme preference
+ Admin User Management
+ - **List Users**: GET /api/auth/users - Get all users with complete info (admin only)
+ - **Delete Account**: DELETE /api/auth/account - Delete own account or admin deletes other accounts
+ Libraries (Admin Only)
+ - **Create Library**: POST /api/libraries - Create new library (Ebooks, Comics, Manga)
+ - **Get Libraries**: GET /api/libraries - List all libraries (admin)
+ - **Get Library**: GET /api/libraries/:id - Get library details
+ - **Update Library**: PUT /api/libraries/:id - Update library settings
+ - **Delete Library**: DELETE /api/libraries/:id - Remove library
+ - **Add Library Folder**: POST /api/libraries/:id/folders - Add scanning folder
+ - **Get Library Folders**: GET /api/libraries/:id/folders - List folders
+ - **Delete Library Folder**: DELETE /api/libraries/:id/folders/:folder_id - Remove folder
+ - **Get Library Stats**: GET /api/libraries/:id/stats - Library statistics
+ - **Get Library Types**: GET /api/libraries/types - Available library types
+ Media Items (Mixed Access)
+ - **List Media Items**: GET /api/media-items - Paginated list (filter/sort by library, author, series, etc.)
+ - **Get Media Item**: GET /api/media-items/:id - Single item details (all users)
+ - **Create Media Item**: POST /api/media-items - Add new item (admin only)
+ - **Update Media Item**: PUT /api/media-items/:id - Modify metadata (admin only)
+ - **Delete Media Item**: DELETE /api/media-items/:id - Remove item (admin only)
+ - **Filter Media Items**: POST /api/media-items/filter - Advanced filtering
+ - **EPUB Download**: GET /api/media-items/:id/download - Download EPUB file
+ - **Cover Image**: GET /api/media-items/:id/cover - Get cover image
+ Reading Progress (All Users)
+ - **Get Progress**: GET /api/progress/:media_id - User's reading progress for media item
+ - **Update Progress**: PUT /api/progress/:media_id - Update reading progress
+ - **Get Device Progress**: GET /api/progress/device/:device_id - Progress by device
+ Universal Progress (All Users)
+ - **Get Universal Progress**: GET /api/universal-progress/:sha256 - Get progress by book hash
+ - **Update Universal Progress**: PUT /api/universal-progress - Update universal progress
+ Notes (All Users)
+ - **Get Notes**: GET /api/notes/:media_id - Get notes for media item
+ - **Create Note**: POST /api/notes - Add new note
+ - **Update Note**: PUT /api/notes/:id - Update note content
+ - **Delete Note**: DELETE /api/notes/:id - Remove note
+ Highlights (All Users)
+ - **Get Highlights**: GET /api/highlights/:media_id - Get highlights for media item
+ - **Create Highlight**: POST /api/highlights - Add new highlight
+ - **Update Highlight**: PUT /api/highlights/:id - Update highlight
+ - **Delete Highlight**: DELETE /api/highlights/:id - Remove highlight
+ Ratings (All Users)
+ - **Get Rating**: GET /api/ratings/:media_id - User's rating (returns 0 if unrated)
+ - **Create/Update Rating**: POST /api/ratings - Rate media item (1-5 stars, half-star precision)
+ - **Delete Rating**: DELETE /api/ratings/:media_id - Remove rating
+ Collections (All Users)
+ - **List Collections**: GET /api/collections - Get user's collections
+ - **Get Collection**: GET /api/collections/:id - Collection details with media items
+ - **Create Collection**: POST /api/collections - Create new collection
+ - **Update Collection**: PUT /api/collections/:id - Update collection
+ - **Delete Collection**: DELETE /api/collections/:id - Remove collection
+ - **Add Auto-Assign Rule**: POST /api/collections/:id/rules - Add automatic rule
+ - **Remove Auto-Assign Rule**: DELETE /api/collections/:id/rules/:rule_id - Remove rule
+ - **Test Rule**: POST /api/collections/:id/rules/test - Preview rule matches
+ - **Bulk Assign**: POST /api/collections/:id/assign - Manually add media items
+ Device Management (All Users)
+ - **Register Device**: POST /api/devices/register - Register new device
+ - **List Devices**: GET /api/devices - Get user's devices
+ - **Get Device**: GET /api/devices/:id - Device details
+ - **Delete Device**: DELETE /api/devices/:id - Unregister device
+ - **Sync Device**: POST /api/devices/:id/sync - Trigger device sync
+ Sync Protocols (Device Integration)
+ - **KOReader Sync**: POST /api/sync/koreader - KOReader progress/notes/highlights sync
+ - **Kobo Sync**: POST /api/sync/kobo - Kobo progress/notes/highlights sync
+ Scanner (Admin Only)
+ - **Scan Libraries**: POST /api/scanner/scan - Scan library folders
+ - **Start Scanner**: POST /api/scanner/start - Start real-time monitoring
+ - **Stop Scanner**: POST /api/scanner/stop - Stop monitoring
+ - **Get Scan Settings**: GET /api/scanner/settings - Scan configuration
+ Analytics (Admin Only)
+ - **Get Analytics**: GET /api/analytics - Usage statistics and metrics
+ Book Matching (All Users)
+ - **Search Books**: GET /api/book-matching/search - Search by ISBN, title, author
+ - **Link Book**: POST /api/book-matching/link - Link media item to external database
+ OPDS (All Users)
+ - **OPDS Feeds**: GET /opds/* - OPDS catalog for e-reader integration
+ - **OPDS Acquisition**: GET /opds/acquisition/* - Download media items
+ WebSocket (Real-time)
+ - **WebSocket**: WS /api/ws - Real-time sync events (progress, notes, highlights)
+ Collection Organization
+ bruno/
+ ├── user/ # User authentication and profile
+ │ ├── auth/ # Login, register, refresh
+ │ ├── profile/ # Profile management
+ │ └── admin/ # User administration (admin only)
+ ├── library/ # Library management
+ │ ├── Create/Update/Delete Libraries
+ │ ├── Library Folders
+ │ ├── Library Stats
+ │ └── Scan Settings
+ ├── media-items/ # Media item operations
+ │ ├── List/Get/Create/Update/Delete
+ │ ├── Filter and Sort
+ │ ├── Download EPUB
+ │ ├── Cover Images
+ │ └── Ratings
+ ├── progress/ # Reading progress tracking
+ ├── universal-progress/ # Cross-device universal progress
+ ├── notes/ # User notes
+ ├── highlights/ # Book highlights
+ ├── collections/ # Smart collections
+ ├── devices/ # Device registration
+ ├── sync-koreader/ # KOReader sync protocol
+ ├── sync-kobo/ # Kobo sync protocol
+ ├── scanner/ # Library scanning
+ ├── analytics/ # Usage statistics
+ ├── books/ # Book matching/linking
+ ├── kobo/ # Kobo-specific operations
+ ├── koreader/ # KOReader-specific operations
+ ├── opds/ # OPDS catalog feeds
+ └── admin/ # Admin operations
+ ## Security Features
+ ### Registration Restrictions
+ - **First User**: Automatically gets admin role regardless of request
+ - **Existing Admins**: Only authenticated admins can create new admin accounts
+ - **Regular Users**: Anyone can create regular user accounts
+ - **Unauthenticated**: Can only create first admin, not subsequent admins
+ ### User Management
+ - **Self-Deletion**: Users can delete their own accounts
+ - **Admin Override**: Admins can delete any user account
+ - **Last Admin Protection**: Cannot delete the last admin account in the system
+ ### Role System
+ - **Admin**: Full access - manage libraries, media items, users, scanner
+ - **User**: Read access - view media items, create collections, track progress, rate, annotate
+ ### Device Authentication
+ - **No Passwords**: Devices use QR code registration and access tokens
+ - **User Approval**: Device registration requires user approval via web interface
+ ### JWT Tokens
+ - **Access Token**: Valid for 1 hour, sent via Bearer header
+ - **Refresh Token**: Valid for 7 days, used to get new access tokens
+ ### Rate Limiting
+ - **Auth Endpoints**: 10 requests/minute per IP
+ ### Data Isolation
+ - **Progress, Notes, Highlights, Ratings**: User-specific
+ - **Collections**: User-specific (admins see all users' collections)
+ - **Devices**: User-specific
+ ## Documentation Features
+ Each request includes:
+ - **Detailed descriptions** of functionality
+ - **Parameter specifications** (required/optional, types)
+ - **Request/Response examples**
+ - **Error response codes** and meanings
+ - **Authentication requirements**
+ ## Notes
+ - **Authentication Flow**: Register → Login → Use Bearer token for all other requests
+ - **Media Items vs Books**: The API uses "media items" (supports ebooks, comics, manga)
+ - **Library System**: Organized by libraries (Ebooks, Comics, Manga) with scanning folders
+ - **Universal Progress**: Cross-device sync using SHA-256 book hashes
+ - **Smart Collections**: Auto-assign rules based on genre, author, series, tags, etc.
+ - **OPDS Support**: Wireless book delivery to e-readers (Kobo, KOReader)
+ - **Device Protocols**: Native sync for KOReader and Kobo devices
+ - **Rating System**: Half-star precision (1-10 scale internally, displayed as 1-5 stars)
+ - **Admin Setup**: First admin must be created by updating user role in database
+ - **Variables**: Update collection variables for testing (media_id, library_id, device_id, etc.)
+ - **Security**: Passwords hashed with bcrypt, unique email/username constraints, role-based access control
+ - **JSON**: All requests/responses use JSON format
+ - **WebSocket**: Real-time events for sync updates across devices
+ type: text/markdown
+bundled: false
+extensions: {}
diff --git a/bruno-yaml/progress/Delete Reading Progress.yml b/bruno-yaml/progress/Delete Reading Progress.yml
new file mode 100644
index 0000000..8951cae
--- /dev/null
+++ b/bruno-yaml/progress/Delete Reading Progress.yml
@@ -0,0 +1,42 @@
+info:
+ name: Delete Reading Progress
+ type: http
+ seq: 1
+http:
+ method: DELETE
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/progress'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+runtime:
+ scripts:
+ - type: tests
+ code: "test_delete_reading_progress_success(status, headers, body) {\n if (status\
+ \ !== 200 && status !== 204) {\n throw new Error(\"Expected status 200\
+ \ or 204, got \" + status);"
+
+docs: |-
+ ## Delete Reading Progress
+
+ Deletes reading progress for a media item. This is a legacy endpoint - consider using universal progress endpoints instead.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/media-items/:id/progress
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `id` (string, required): Media item UUID
+
+ **Response:** Success message or empty
+
+ **Status Codes:**
+ - 200: Success
+ - 204: Success (no content)
+ - 401: Unauthorized
+ - 404: Media item not found
+ - 500: Internal server error
+
+ **Note:** This is a legacy endpoint. Use `/api/progress/:id` endpoints for new implementations.
diff --git a/bruno-yaml/progress/Get Reading Progress.yml b/bruno-yaml/progress/Get Reading Progress.yml
new file mode 100644
index 0000000..ced0b52
--- /dev/null
+++ b/bruno-yaml/progress/Get Reading Progress.yml
@@ -0,0 +1,30 @@
+info:
+ name: Get Reading Progress
+ type: http
+ seq: 6
+http:
+ method: GET
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/progress'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get Reading Progress
+
+ Retrieves the authenticated user's reading progress for a media item.
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `id` (string): Media Item UUID
+
+ **Response:**
+ - `id` (string): Media Item UUID
+ - `user_id` (string): User UUID
+ - `current_page` (number): Current page number
+ - `total_pages` (number, nullable): Total pages
+ - `last_read_at` (string): Last read timestamp
+
+ **Error Responses:**
+ - 401: Invalid authentication
diff --git a/bruno-yaml/progress/Update Reading Progress.yml b/bruno-yaml/progress/Update Reading Progress.yml
new file mode 100644
index 0000000..546a49e
--- /dev/null
+++ b/bruno-yaml/progress/Update Reading Progress.yml
@@ -0,0 +1,31 @@
+info:
+ name: Update Reading Progress
+ type: http
+ seq: 7
+http:
+ method: PUT
+ url: '{{base_url}}/api/media-items/{{media_item_id}}/progress'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"current_page\": 45,\n \"total_pages\": 200"
+
+docs: |-
+ ## Update Reading Progress
+
+ Updates the authenticated user's reading progress for a media item.
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `id` (string): Media Item UUID
+
+ **Request Body:**
+ - `current_page` (number, required): Current page number
+ - `total_pages` (number, optional): Total pages in book
+
+ **Response:** Updated progress object
+
+ **Error Responses:**
+ - 401: Invalid authentication
+ - 400: Invalid request data
diff --git a/bruno-yaml/queue/Clear Device Queue.yml b/bruno-yaml/queue/Clear Device Queue.yml
new file mode 100644
index 0000000..eb442a0
--- /dev/null
+++ b/bruno-yaml/queue/Clear Device Queue.yml
@@ -0,0 +1,39 @@
+info:
+ name: Clear Device Queue
+ type: http
+ seq: 6
+http:
+ method: DELETE
+ url: '{{base_url}}/api/queue/devices/{{device_id}}/clear'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Clear Device Queue
+
+ Clears all queue items for a specific device.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/queue/devices/:device_id/clear
+
+ **Authentication:** Bearer token
+
+ **Path Parameters:**
+ - `device_id` (string): Device UUID
+
+ **Response:**
+ - Success message with count of cleared items
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Device not found
+
+ **Example Response:**
+ ```json
+ {
+ "message": "Queue cleared",
+ "device_id": "uuid",
+ "cleared_count": 10
diff --git a/bruno-yaml/queue/Clear Failed Items.yml b/bruno-yaml/queue/Clear Failed Items.yml
new file mode 100644
index 0000000..cee1d75
--- /dev/null
+++ b/bruno-yaml/queue/Clear Failed Items.yml
@@ -0,0 +1,87 @@
+info:
+ name: Clear Failed Items
+ type: http
+ seq: 8
+
+http:
+ method: DELETE
+ url: '{{base_url}}/api/queue/clear-failed'
+ auth: inherit
+
+docs: |-
+ ## Clear Failed Queue Items
+
+ Removes all failed queue items from the queue.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/queue/clear-failed
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `older_than` (string, optional): ISO 8601 duration - e.g., `7d`, `24h`, `60m`
+ - `device_id` (string, optional): Only clear items from specific device
+ - `type` (string, optional): Only clear specific item type
+
+ **Request Body:**
+ Optional filters:
+ ```json
+ {
+ "older_than": "7d",
+ "device_id": "uuid",
+ "type": "progress"
+ }
+ ```
+
+ **Response:**
+ - `deleted_count` (integer): Number of items deleted
+ - `items_cleared` (array): IDs of cleared items
+ - `retained_count` (integer): Items not matching filters
+ - `cleared_at` (string): Deletion timestamp
+
+ **Status Codes:**
+ - 200: Items cleared successfully
+ - 401: Unauthorized
+ - 400: Invalid filter parameters
+
+ **Deletion Behavior:**
+ - Removes failed items from queue
+ - Does not affect pending/processing/completed items
+ - Permanent deletion (cannot be undone)
+ - Logs deletion for audit trail
+ - Updates queue statistics
+
+ **Safety Features:**
+ - Default filter prevents clearing recent failures
+ - Per-device filtering for targeted cleanup
+ - Type filtering for selective clearing
+ - Confirmation required for large deletions
+
+ **Recommended Usage:**
+ - **Daily:** Clear failures older than 7 days
+ - **Weekly:** Clear all failed items
+ - **Per-device:** After removing problematic device
+ - **Per-type:** After fixing specific sync issue
+
+ **Use Cases:**
+ - Clean up old failed sync attempts
+ - Reduce queue database size
+ - Remove stuck error items
+ - Prepare for fresh sync attempts
+ - Clear items for decommissioned devices
+ - Reset after bug fixes
+
+ **Warnings:**
+ - Cannot be undone
+ - Failed items cleared permanently
+ - Consider retrying before clearing
+ - May hide recurring issues if overused
+ - Check logs before bulk clearing
+
+ **Best Practices:**
+ - Review error patterns before clearing
+ - Fix root causes before cleanup
+ - Use time-based filters to preserve recent failures
+ - Document reasons for clearing
+ - Monitor failure rates after cleanup
diff --git a/bruno-yaml/queue/Delete Queue Item.yml b/bruno-yaml/queue/Delete Queue Item.yml
new file mode 100644
index 0000000..9f90c02
--- /dev/null
+++ b/bruno-yaml/queue/Delete Queue Item.yml
@@ -0,0 +1,38 @@
+info:
+ name: Delete Queue Item
+ type: http
+ seq: 5
+http:
+ method: DELETE
+ url: '{{base_url}}/api/queue/items/{{item_id}}'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Delete Queue Item
+
+ Deletes a queue item from the sync queue.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/queue/items/:item_id
+
+ **Authentication:** Bearer token
+
+ **Path Parameters:**
+ - `item_id` (string): Queue item UUID
+
+ **Response:**
+ - Success message confirming deletion
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Queue item not found
+
+ **Example Response:**
+ ```json
+ {
+ "message": "Queue item deleted",
+ "item_id": "uuid"
diff --git a/bruno-yaml/queue/Get Queue Statistics.yml b/bruno-yaml/queue/Get Queue Statistics.yml
new file mode 100644
index 0000000..5aec542
--- /dev/null
+++ b/bruno-yaml/queue/Get Queue Statistics.yml
@@ -0,0 +1,83 @@
+info:
+ name: Get Queue Statistics
+ type: http
+ seq: 9
+
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/stats'
+ auth: inherit
+
+docs: |-
+ ## Get Queue Statistics
+
+ Retrieves overall queue performance and status metrics.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/stats
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ None (all stats returned)
+
+ **Response:**
+ - `overview` (object):
+ - `total_items` (integer): All items in queue
+ - `pending` (integer): Items awaiting processing
+ - `processing` (integer): Items currently being processed
+ - `completed` (integer): Successfully processed items
+ - `failed` (integer): Failed items
+ - `by_type` (object):
+ - `progress` (object): Progress item counts
+ - `note` (object): Note item counts
+ - `highlight` (object): Highlight item counts
+ - `bookmark` (object): Bookmark item counts
+ - `by_status` (object):
+ - `pending`: Count by type
+ - `processing`: Count by type
+ - `completed`: Count by type
+ - `failed`: Count by type
+ - `performance` (object):
+ - `avg_processing_time_ms` (number): Average processing duration
+ - `throughput_per_minute` (number): Items processed per minute
+ - `success_rate` (number): Percentage of successful processing
+ - `failure_rate` (number): Percentage of failures
+ - `timing` (object):
+ - `oldest_pending_age_seconds` (integer): Age of oldest pending item
+ - `longest_processing_time_ms` (integer): Longest current processing time
+ - `peak_queue_size_today` (integer): Maximum queue size today
+ - `devices` (array): Top 5 devices by queue activity
+ - `device_id` (string): Device UUID
+ - `device_name` (string): Device name
+ - `item_count` (integer): Items in queue from this device
+ - `retention` (object):
+ - `completed_items_30_days` (integer): Completed items in retention period
+ - `purge_scheduled_at` (string): Next auto-purge time
+ - `generated_at` (string): Statistics generation timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Cache Behavior:**
+ - Statistics cached for 60 seconds
+ - Real-time data may vary slightly
+ - Use `?refresh=true` to bypass cache
+
+ **Use Cases:**
+ - Monitor queue health
+ - Identify performance bottlenecks
+ - Track processing capacity
+ - Alert on failures
+ - Capacity planning
+ - SLA monitoring
+ - Dashboard metrics
+
+ **Key Metrics to Watch:**
+ - **Success Rate:** Should be >95%
+ - **Avg Processing Time:** Depends on type
+ - **Oldest Pending:** Should be <5 minutes
+ - **Failed Items:** Investigate if >5%
+ - **Throughput:** Baseline for capacity planning
diff --git a/bruno-yaml/queue/List All Queue Items (Admin).yml b/bruno-yaml/queue/List All Queue Items (Admin).yml
new file mode 100644
index 0000000..b0d3138
--- /dev/null
+++ b/bruno-yaml/queue/List All Queue Items (Admin).yml
@@ -0,0 +1,39 @@
+info:
+ name: List All Queue Items (Admin)
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/items'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## List All Queue Items (Admin)
+
+ Retrieves all queue items across all devices (admin only).
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/items
+
+ **Authentication:** Bearer token (admin role required)
+
+ **Response:**
+ - Array of queue items with device and status information
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Forbidden - admin role required
+
+ **Example Response:**
+ ```json
+ [
+ {
+ "id": "uuid",
+ "device_id": "uuid",
+ "item_type": "progress",
+ "status": "pending",
+ "created_at": "2024-01-01T00:00:00Z"
diff --git a/bruno-yaml/queue/List Device Queue Items.yml b/bruno-yaml/queue/List Device Queue Items.yml
new file mode 100644
index 0000000..a8580b2
--- /dev/null
+++ b/bruno-yaml/queue/List Device Queue Items.yml
@@ -0,0 +1,42 @@
+info:
+ name: List Device Queue Items
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/devices/{{device_id}}/items'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## List Device Queue Items
+
+ Retrieves all queue items for a specific device.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/devices/:device_id/items
+
+ **Authentication:** Bearer token
+
+ **Path Parameters:**
+ - `device_id` (string): Device UUID
+
+ **Response:**
+ - Array of queue items for the specified device
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Device not found
+
+ **Example Response:**
+ ```json
+ [
+ {
+ "id": "uuid",
+ "device_id": "uuid",
+ "item_type": "progress",
+ "status": "pending",
+ "data": {
diff --git a/bruno-yaml/queue/List Queue Items (Pagination).yml b/bruno-yaml/queue/List Queue Items (Pagination).yml
new file mode 100644
index 0000000..f1c5a00
--- /dev/null
+++ b/bruno-yaml/queue/List Queue Items (Pagination).yml
@@ -0,0 +1,53 @@
+info:
+ name: List Queue Items (With Pagination)
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{base_url}}/queue/items?limit=50&offset=0'
+ auth: inherit
+ body:
+ type: none
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ List items in the sync queue with pagination.
+
+ **Endpoint**: GET /queue/items
+ **Auth**: Required (Bearer token)
+
+ ## Query Parameters
+
+ | Parameter | Type | Required | Description |
+ |-----------|------|-----------|-------------|
+ | limit | int | No | Items per page |
+ | offset | int | No | Pagination offset |
+
+ ## Response Fields
+
+ | Field | Type | Description |
+ |-------|------|-------------|
+ | items | array | List of queue items |
+ | total | int | Total number of items |
+ | page | int | Current page number |
+ | per_page | int | Items per page |
+
+ ## Example Request
+
+ ```
+ GET /queue/items?limit=50&offset=0
+ ```
+
+ ## Error Responses
+
+ | Code | Description |
+ |------|-------------|
+ | 401 | Unauthorized |
+ | 500 | Internal server error |
+
+ ## Notes
+
+ - Supports pagination for large queue lists
diff --git a/bruno-yaml/queue/Process Queue Item.yml b/bruno-yaml/queue/Process Queue Item.yml
new file mode 100644
index 0000000..c31f9bb
--- /dev/null
+++ b/bruno-yaml/queue/Process Queue Item.yml
@@ -0,0 +1,74 @@
+info:
+ name: Process Queue Item
+ type: http
+ seq: 7
+
+http:
+ method: POST
+ url: '{{base_url}}/api/queue/items/{{queue_item_id}}/process'
+ auth: inherit
+
+docs: |-
+ ## Process Queue Item
+
+ Manually trigger processing of a specific queue item.
+
+ **Method:** POST
+
+ **Endpoint:** /api/queue/items/:queue_item_id/process
+
+ **Authentication:** Required (Bearer token)
+
+ **Path Parameters:**
+ - `queue_item_id` (string): UUID of the queue item to process
+
+ **Request Body:**
+ Empty or optional:
+ - `priority` (integer, optional): Override processing priority (0-10)
+ - `force` (boolean, optional): Re-process even if completed
+
+ **Response:**
+ - `id` (string): Queue item UUID
+ - `status` (string): New status - `processing` or `queued`
+ - `type` (string): Item type
+ - `message` (string): Processing initiation message
+ - `estimated_duration_ms` (integer): Expected processing time
+ - `started_at` (string): Processing start time
+ - `previous_status` (string): Status before processing
+
+ **Status Codes:**
+ - 200: Processing started
+ - 202: Queued for processing
+ - 401: Unauthorized
+ - 404: Queue item not found
+ - 409: Item already being processed
+ - 422: Invalid item state
+
+ **Processing Behavior:**
+ - Item moves from `pending` to `processing`
+ - Can override priority for faster processing
+ - Force re-processing of completed/failed items
+ - Updates item's `last_attempt` timestamp
+ - Increments `retry_count` for failed items
+ - Validates data before processing
+ - Executes item-specific handlers
+
+ **Use Cases:**
+ - Retry failed sync items
+ - Expedite high-priority updates
+ - Re-process completed items (e.g., after fix)
+ - Debug queue processing
+ - Manual intervention for stuck items
+ - Test specific sync operations
+
+ **Processing States:**
+ - `pending` → `processing` → `completed` or `failed`
+ - Processing can take 1-30 seconds depending on type
+ - WebSocket notifications sent on completion
+ - Automatic retry on transient failures
+
+ **Notes:**
+ - Does not guarantee immediate processing
+ - Queue workers pick up items based on priority
+ - Multiple manual processes queue sequentially
+ - Use WebSocket for real-time updates
diff --git a/bruno-yaml/queue/Retry Queue Item.yml b/bruno-yaml/queue/Retry Queue Item.yml
new file mode 100644
index 0000000..e472f14
--- /dev/null
+++ b/bruno-yaml/queue/Retry Queue Item.yml
@@ -0,0 +1,39 @@
+info:
+ name: Retry Queue Item
+ type: http
+ seq: 4
+http:
+ method: POST
+ url: '{{base_url}}/api/queue/items/{{item_id}}/retry'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Retry Queue Item
+
+ Retries a failed queue item.
+
+ **Method:** POST
+
+ **Endpoint:** /api/queue/items/:item_id/retry
+
+ **Authentication:** Bearer token
+
+ **Path Parameters:**
+ - `item_id` (string): Queue item UUID
+
+ **Response:**
+ - Success message indicating retry initiated
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Queue item not found
+ - 400: Invalid item status
+
+ **Example Response:**
+ ```json
+ {
+ "message": "Queue item retry initiated",
+ "item_id": "uuid"
diff --git a/bruno-yaml/queue/api.yml b/bruno-yaml/queue/api.yml
new file mode 100644
index 0000000..9274fb3
--- /dev/null
+++ b/bruno-yaml/queue/api.yml
@@ -0,0 +1,7 @@
+info:
+ name: Bookhoard Sync Queue API
+ type: collection
+ seq: 1
+http:
+ method: GET
+ url: '"http://localhost:8765/api"'
diff --git a/bruno-yaml/queue/scenarios/Filter by Status - Completed.yml b/bruno-yaml/queue/scenarios/Filter by Status - Completed.yml
new file mode 100644
index 0000000..e7e8b48
--- /dev/null
+++ b/bruno-yaml/queue/scenarios/Filter by Status - Completed.yml
@@ -0,0 +1,59 @@
+info:
+ name: Filter by Status - Completed
+ type: http
+ seq: 2
+
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/items?status=completed'
+ auth: inherit
+
+docs: |-
+ ## Filter Queue Items by Completed Status
+
+ Retrieves all queue items that have been successfully processed.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/items?status=completed
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `status` (string): Must be `completed`
+ - `limit` (integer, optional): Max items to return (default: 50)
+ - `offset` (integer, optional): Number of items to skip
+ - `sort_by` (string, optional): Sort field - `created_at`, `completed_at`
+ - `sort_order` (string, optional): `asc` or `desc` (default: `desc`)
+
+ **Response:**
+ - `items` (array): Completed queue items
+ - `id` (string): Queue item UUID
+ - `type` (string): Item type
+ - `status` (string): `completed`
+ - `created_at` (string): When item was queued
+ - `completed_at` (string): When processing finished
+ - `duration_ms` (integer): Processing time
+ - `device_id` (string): Device that queued the item
+ - `media_id` (string): Associated media item
+ - `result` (object): Processing result details
+ - `total` (integer): Total completed items
+ - `page` (object): Pagination info
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Retention Policy:**
+ - Completed items retained for 30 days
+ - Auto-purged after retention period
+ - Can be exported before deletion
+ - Statistics aggregated before purge
+
+ **Use Cases:**
+ - Audit sync history
+ - Monitor system performance
+ - Analyze processing times
+ - Verify successful operations
+ - Generate compliance reports
+ - Track device activity patterns
diff --git a/bruno-yaml/queue/scenarios/Filter by Status - Failed.yml b/bruno-yaml/queue/scenarios/Filter by Status - Failed.yml
new file mode 100644
index 0000000..e7cf5d0
--- /dev/null
+++ b/bruno-yaml/queue/scenarios/Filter by Status - Failed.yml
@@ -0,0 +1,59 @@
+info:
+ name: Filter by Status - Failed
+ type: http
+ seq: 1
+
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/items?status=failed'
+ auth: inherit
+
+docs: |-
+ ## Filter Queue Items by Failed Status
+
+ Retrieves all queue items that have failed to process.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/items?status=failed
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `status` (string): Must be `failed`
+ - `limit` (integer, optional): Max items to return (default: 50)
+ - `offset` (integer, optional): Number of items to skip (for pagination)
+
+ **Response:**
+ - `items` (array): Failed queue items
+ - `id` (string): Queue item UUID
+ - `type` (string): Item type - `progress`, `note`, `highlight`, `bookmark`
+ - `status` (string): `failed`
+ - `error_message` (string): Error details
+ - `error_code` (string): Error classification
+ - `retry_count` (integer): Number of retry attempts
+ - `max_retries` (integer): Maximum allowed retries
+ - `created_at` (string): When item was queued
+ - `failed_at` (string): When the item failed
+ - `device_id` (string): Device that queued the item
+ - `media_id` (string): Associated media item
+ - `total` (integer): Total failed items
+ - `page` (object): Pagination info
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Common Failure Reasons:**
+ - Network timeout during sync
+ - Device offline during processing
+ - Invalid data format
+ - Database constraint violations
+ - External service unavailability
+
+ **Use Cases:**
+ - Monitor sync failures
+ - Identify problematic devices
+ - Debug integration issues
+ - Track error patterns
+ - Determine items needing manual retry
diff --git a/bruno-yaml/queue/scenarios/Filter by Status - Pending.yml b/bruno-yaml/queue/scenarios/Filter by Status - Pending.yml
new file mode 100644
index 0000000..627ceb3
--- /dev/null
+++ b/bruno-yaml/queue/scenarios/Filter by Status - Pending.yml
@@ -0,0 +1,46 @@
+info:
+ name: Filter by Status - Pending
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/queue/items?status=pending'
+ auth: inherit
+ body:
+ type: none
+runtime:
+ scripts:
+ - type: tests
+ code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
+
+docs: |-
+ Filter queue items by status - show only pending items.
+
+ **Endpoint**: GET /queue/items?status=pending
+ **Auth**: Required (Bearer token)
+
+ ## Query Parameters
+
+ | Parameter | Type | Required | Description |
+ |-----------|------|-----------|-------------|
+ | status | string | Yes | Status filter: pending, processing, completed, failed |
+
+ ## Response Fields
+
+ | Field | Type | Description |
+ |-------|------|-------------|
+ | items | array | List of queue items with pending status |
+ | total | int | Total matching items |
+
+ ## Example Request
+
+ ```
+ GET /queue/items?status=pending
+ ```
+
+ ## Example Response
+
+ ```json
+ {
+ "items": [...],
+ "total": 15
diff --git a/bruno-yaml/queue/scenarios/Filter by Type - Bookmarks.yml b/bruno-yaml/queue/scenarios/Filter by Type - Bookmarks.yml
new file mode 100644
index 0000000..b25b7d4
--- /dev/null
+++ b/bruno-yaml/queue/scenarios/Filter by Type - Bookmarks.yml
@@ -0,0 +1,84 @@
+info:
+ name: Filter by Type - Bookmarks
+ type: http
+ seq: 6
+
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/items?status=all&type=bookmark'
+ auth: inherit
+
+docs: |-
+ ## Filter Queue Items by Bookmarks Type
+
+ Retrieves all bookmark sync items in the queue.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/items?status=all&type=bookmark
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `status` (string): Use `all` to get all statuses
+ - `type` (string): Must be `bookmark`
+ - `media_id` (string, optional): Filter by media item
+ - `device_id` (string, optional): Filter by device
+
+ **Response:**
+ - `items` (array): Bookmark queue items
+ - `id` (string): Queue item UUID
+ - `type` (string): `bookmark`
+ - `status` (string): Queue status
+ - `bookmark_data` (object):
+ - `media_id` (string): Associated media
+ - `device_id` (string): Source device
+ - `bookmark_id` (string): Bookmark identifier
+ - `title` (string): Bookmark title (optional)
+ - `position` (object):
+ - `chapter` (string): Chapter reference
+ - `page` (integer): Page number
+ - `location` (string): Epub location
+ - `percentage` (number): Position percentage
+ - `content_path` (string): Internal path (KOReader)
+ - `progress` (string): Progress indicator
+ - `created_at` (string): Bookmark creation time
+ - `modified_at` (string): Last edit time
+ - `notes` (string, optional): Bookmark notes
+ - `action` (string): `create`, `update`, or `delete`
+ - `priority` (integer): Processing priority
+ - `created_at` (string): Queued timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Bookmark Features:**
+ - Custom bookmark titles
+ - Chapter-level bookmarks
+ - Precise location tracking
+ - Progress preservation
+ - Cross-device sync
+ - Hierarchical organization (KOReader)
+ - Kindle location support
+ - Kobo chapter marks
+
+ **Device-Specific Features:**
+ - **Kobo:** Chapter bookmarks, automatic marks
+ - **KOReader:** Hierarchical bookmarks, custom titles, notes
+ - **Kindle:** Location-based, page numbers
+ - **Web:** Chapter-based, custom titles
+
+ **Bookmark Types:**
+ - User-created manual bookmarks
+ - Auto-generated chapter marks
+ - Last read position
+ - Progress milestones (25%, 50%, 75%)
+ - Custom collection markers
+
+ **Use Cases:**
+ - Monitor bookmark sync
+ - Track reading progress points
+ - Debug location mapping
+ - Verify cross-device bookmarks
+ - Export reading positions
diff --git a/bruno-yaml/queue/scenarios/Filter by Type - Highlights.yml b/bruno-yaml/queue/scenarios/Filter by Type - Highlights.yml
new file mode 100644
index 0000000..e1ba9b7
--- /dev/null
+++ b/bruno-yaml/queue/scenarios/Filter by Type - Highlights.yml
@@ -0,0 +1,79 @@
+info:
+ name: Filter by Type - Highlights
+ type: http
+ seq: 5
+
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/items?status=all&type=highlight'
+ auth: inherit
+
+docs: |-
+ ## Filter Queue Items by Highlights Type
+
+ Retrieves all highlight sync items in the queue.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/items?status=all&type=highlight
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `status` (string): Use `all` to get all statuses
+ - `type` (string): Must be `highlight`
+ - `media_id` (string, optional): Filter by media item
+ - `device_id` (string, optional): Filter by device
+ - `color` (string, optional): Filter by highlight color
+
+ **Response:**
+ - `items` (array): Highlight queue items
+ - `id` (string): Queue item UUID
+ - `type` (string): `highlight`
+ - `status` (string): Queue status
+ - `highlight_data` (object):
+ - `media_id` (string): Associated media
+ - `device_id` (string): Source device
+ - `highlight_id` (string): Highlight identifier
+ - `text` (string): Highlighted text
+ - `color` (string): Highlight color (hex or name)
+ - `note` (string, optional): Attached note
+ - `position` (object):
+ - `chapter` (string): Chapter reference
+ - `page` (integer): Page number
+ - `location` (string): Epub location
+ - `percentage` (number): Position percentage
+ - `start_position` (string): Selection start
+ - `end_position` (string): Selection end
+ - `text_offset` (integer): Character offset
+ - `created_at` (string): Highlight creation time
+ - `modified_at` (string): Last edit time
+ - `action` (string): `create`, `update`, or `delete`
+ - `priority` (integer): Processing priority
+ - `created_at` (string): Queued timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Highlight Features:**
+ - Multi-color support (yellow, green, blue, pink, orange)
+ - Precise text selection tracking
+ - Linked notes support
+ - Chapter and page location
+ - Character-level precision
+ - Cross-device color preservation
+ - KOReader custom colors supported
+
+ **Device-Specific Color Support:**
+ - **Kobo:** 5 preset colors (yellow, green, blue, pink, orange)
+ - **KOReader:** Custom RGB colors
+ - **Kindle:** Yellow, blue, orange
+ - **Web:** Full color palette
+
+ **Use Cases:**
+ - Track highlight synchronization
+ - Debug color mapping issues
+ - Monitor annotation activity
+ - Analyze reading patterns
+ - Export highlights by device
diff --git a/bruno-yaml/queue/scenarios/Filter by Type - Notes.yml b/bruno-yaml/queue/scenarios/Filter by Type - Notes.yml
new file mode 100644
index 0000000..fa21d79
--- /dev/null
+++ b/bruno-yaml/queue/scenarios/Filter by Type - Notes.yml
@@ -0,0 +1,74 @@
+info:
+ name: Filter by Type - Notes
+ type: http
+ seq: 4
+
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/items?status=all&type=note'
+ auth: inherit
+
+docs: |-
+ ## Filter Queue Items by Notes Type
+
+ Retrieves all note sync items in the queue.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/items?status=all&type=note
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `status` (string): Use `all` to get all statuses
+ - `type` (string): Must be `note`
+ - `media_id` (string, optional): Filter by media item
+ - `device_id` (string, optional): Filter by device
+
+ **Response:**
+ - `items` (array): Note queue items
+ - `id` (string): Queue item UUID
+ - `type` (string): `note`
+ - `status` (string): Queue status
+ - `note_data` (object):
+ - `media_id` (string): Associated media
+ - `device_id` (string): Source device
+ - `note_id` (string): Note identifier
+ - `text` (string): Note content
+ - `color` (string, optional): Note color (hex)
+ - `highlight_id` (string, optional): Parent highlight
+ - `position` (object): Location in book
+ - `chapter` (string): Chapter reference
+ - `page` (integer): Page number
+ - `location` (string): Epub location
+ - `percentage` (number): Position percentage
+ - `created_at` (string): Note creation time
+ - `modified_at` (string): Last edit time
+ - `action` (string): `create`, `update`, or `delete`
+ - `priority` (integer): Processing priority
+ - `created_at` (string): Queued timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Note Features:**
+ - Rich text content support
+ - Color-coded notes
+ - Linked to highlights
+ - Precise location tracking
+ - Cross-device sync
+ - Edit history preserved
+ - Anonymous notes supported
+
+ **Actions:**
+ - `create`: New note from device
+ - `update`: Modified note content
+ - `delete`: Removed note
+
+ **Use Cases:**
+ - Monitor note synchronization
+ - Debug note sync failures
+ - Track reading activity
+ - Analyze annotation patterns
+ - Verify content propagation
diff --git a/bruno-yaml/queue/scenarios/Filter by Type - Progress.yml b/bruno-yaml/queue/scenarios/Filter by Type - Progress.yml
new file mode 100644
index 0000000..a841fbc
--- /dev/null
+++ b/bruno-yaml/queue/scenarios/Filter by Type - Progress.yml
@@ -0,0 +1,71 @@
+info:
+ name: Filter by Type - Progress
+ type: http
+ seq: 3
+
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/items?status=all&type=progress'
+ auth: inherit
+
+docs: |-
+ ## Filter Queue Items by Progress Type
+
+ Retrieves all reading progress sync items in the queue.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/items?status=all&type=progress
+
+ **Authentication:** Required (Bearer token)
+
+ **Query Parameters:**
+ - `status` (string): Use `all` to get all statuses
+ - `type` (string): Must be `progress`
+ - `media_id` (string, optional): Filter by specific media item
+ - `device_id` (string, optional): Filter by specific device
+ - `limit` (integer, optional): Max items to return
+
+ **Response:**
+ - `items` (array): Progress queue items
+ - `id` (string): Queue item UUID
+ - `type` (string): `progress`
+ - `status` (string): `pending`, `processing`, `completed`, `failed`
+ - `progress_data` (object):
+ - `media_id` (string): Book/media identifier
+ - `device_id` (string): Source device
+ - `percentage` (integer): Reading progress 0-100
+ - `position` (string): Location in book
+ - `page_number` (integer, optional): Current page
+ - `chapter` (string, optional): Current chapter
+ - `finished` (boolean): Book completed flag
+ - `timestamp` (string): When progress was recorded
+ - `priority` (integer): Processing priority (0-10)
+ - `created_at` (string): Queued timestamp
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+
+ **Progress Sync Features:**
+ - Cross-device progress synchronization
+ - SHA-256 based book identification (KOReader)
+ - Location-based progress (Kobo, Kindle)
+ - Page number tracking
+ - Chapter bookmarking
+ - Finished status propagation
+ - Reading time calculation
+
+ **Priority Levels:**
+ - 10: Manual sync requests
+ - 8: Device-initiated sync
+ - 5: Scheduled automatic sync
+ - 3: Background updates
+ - 1: Bulk operations
+
+ **Use Cases:**
+ - Track pending progress updates
+ - Monitor sync health across devices
+ - Debug progress sync issues
+ - Identify devices with stale data
+ - Verify progress propagation
diff --git a/bruno-yaml/queue/scenarios/Get Device Queue Stats.yml b/bruno-yaml/queue/scenarios/Get Device Queue Stats.yml
new file mode 100644
index 0000000..e469480
--- /dev/null
+++ b/bruno-yaml/queue/scenarios/Get Device Queue Stats.yml
@@ -0,0 +1,41 @@
+info:
+ name: Get Device Queue Stats
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{base_url}}/api/queue/devices/{{device_id}}/stats'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get Device Queue Stats
+
+ Retrieves statistics for a specific device's sync queue.
+
+ **Method:** GET
+
+ **Endpoint:** /api/queue/devices/:device_id/stats
+
+ **Authentication:** Bearer token
+
+ **Path Parameters:**
+ - `device_id` (string): Device UUID
+
+ **Response:**
+ - Queue statistics including pending, completed, and failed counts
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 404: Device not found
+
+ **Example Response:**
+ ```json
+ {
+ "device_id": "uuid",
+ "total_items": 10,
+ "pending": 3,
+ "completed": 5,
+ "failed": 2
diff --git a/bruno-yaml/scanner/Get Scan Status.yml b/bruno-yaml/scanner/Get Scan Status.yml
new file mode 100644
index 0000000..d1f49f0
--- /dev/null
+++ b/bruno-yaml/scanner/Get Scan Status.yml
@@ -0,0 +1,33 @@
+info:
+ name: Get Scan Status
+ type: http
+ seq: 4
+http:
+ method: GET
+ url: '{{base_url}}/api/scanner/status/{{job_id}}'
+ auth: inherit
+
+docs: |-
+ ## Get Scan Status
+
+ Retrieves the status and progress of an asynchronous scan job.
+
+ **Method:** GET
+
+ **Endpoint:** /api/scanner/status/:jobId
+
+ **Authentication:** Required (Bearer token, Admin only)
+
+ **URL Parameters:**
+ - `jobId` (string, required): The job ID returned from the scan endpoint
+ - Example: `550e8400-e29b-41d4-a716-446655440000`
+
+ **Response:**
+ ```json
+ {
+ "job_id": "550e8400-e29b-41d4-a716-446655440000",
+ "status": "completed",
+ "error": "",
+ "result": {
+ "message": "scan completed",
+ "library_id": "550e8400-e29b-41d4-a716-446655440000"
diff --git a/bruno-yaml/scanner/Get Watch Mode Status.yml b/bruno-yaml/scanner/Get Watch Mode Status.yml
new file mode 100644
index 0000000..06d3f0f
--- /dev/null
+++ b/bruno-yaml/scanner/Get Watch Mode Status.yml
@@ -0,0 +1,28 @@
+info:
+ name: Get Watch Mode Status
+ type: http
+ seq: 7
+http:
+ method: GET
+ url: '{{base_url}}/api/scanner/watch/status'
+ auth: inherit
+
+docs: |-
+ ## Get Watch Mode Status
+
+ Retrieves the current status of watch mode for all libraries.
+
+ **Method:** GET
+
+ **Endpoint:** /api/scanner/watch/status
+
+ **Authentication:** Required (Bearer token, Admin only)
+
+ **Response:** HTTP 200 (OK)
+ ```json
+ {
+ "watching_libraries": [
+ "550e8400-e29b-41d4-a716-446655440000",
+ "660e8400-e29b-41d4-a716-446655440001"
+ ],
+ "total_watching": 2
diff --git a/bruno-yaml/scanner/Scan Media Items.yml b/bruno-yaml/scanner/Scan Media Items.yml
new file mode 100644
index 0000000..73ac5e8
--- /dev/null
+++ b/bruno-yaml/scanner/Scan Media Items.yml
@@ -0,0 +1,33 @@
+info:
+ name: Scan Media Items (Background)
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/scanner/scan'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"folder_paths\": [\"/path/to/media\"]"
+
+docs: |-
+ ## Scan Media Items (Background)
+
+ Triggers an asynchronous media items scanning operation. The scan runs in the background and can be monitored using the job ID.
+
+ **Method:** POST
+
+ **Endpoint:** /api/scanner/scan
+
+ **Authentication:** Required (Bearer token, Admin only)
+
+ **Request Body:**
+ - `folder_paths` (array of strings, required): List of folder paths to scan
+ - Example: `["/path/to/media", "/another/path"]`
+
+ **Response:** HTTP 202 (Accepted)
+ ```json
+ {
+ "message": "scan job enqueued",
+ "job_id": "550e8400-e29b-41d4-a716-446655440000",
+ "status": "pending"
diff --git a/bruno-yaml/scanner/Start Scanner.yml b/bruno-yaml/scanner/Start Scanner.yml
new file mode 100644
index 0000000..153bf4e
--- /dev/null
+++ b/bruno-yaml/scanner/Start Scanner.yml
@@ -0,0 +1,32 @@
+info:
+ name: Start Scanner
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/scanner/start'
+ auth: inherit
+
+docs: |-
+ ## Start Scanner
+
+ Starts the media scanner service for indexing library content.
+
+ **Method:** POST
+
+ **Endpoint:** /api/scanner/start
+
+ **Authentication:** Required (Bearer token)
+
+ **Response:**
+ - JSON object containing scanner status
+ - `status` (string): Scanner state ("started", "running")
+ - `message` (string): Status message
+ - `started_at` (string): Timestamp when scanner started
+
+ **Status Codes:**
+ - 200: Scanner started successfully
+ - 401: Unauthorized
+ - 403: Forbidden (insufficient permissions)
+ - 409: Scanner already running
+ - 500: Internal server error
diff --git a/bruno-yaml/scanner/Start Watch Mode.yml b/bruno-yaml/scanner/Start Watch Mode.yml
new file mode 100644
index 0000000..3a902b0
--- /dev/null
+++ b/bruno-yaml/scanner/Start Watch Mode.yml
@@ -0,0 +1,32 @@
+info:
+ name: Start Watch Mode
+ type: http
+ seq: 5
+http:
+ method: POST
+ url: '{{base_url}}/api/scanner/watch/start'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"library_id\": \"{{library_id"
+
+docs: |-
+ ## Start Watch Mode
+
+ Starts real-time file system monitoring for a specific library. New media items will be detected and processed almost instantly.
+
+ **Method:** POST
+
+ **Endpoint:** /api/scanner/watch/start
+
+ **Authentication:** Required (Bearer token, Admin only)
+
+ **Request Body:**
+ - `library_id` (string, required): UUID of the library to watch
+ - Example: `"550e8400-e29b-41d4-a716-446655440000"`
+
+ **Response:** HTTP 200 (OK)
+ ```json
+ {
+ "message": "watch mode started for library",
+ "library_id": "550e8400-e29b-41d4-a716-446655440000"
diff --git a/bruno-yaml/scanner/Stop Scanner.yml b/bruno-yaml/scanner/Stop Scanner.yml
new file mode 100644
index 0000000..6f736b5
--- /dev/null
+++ b/bruno-yaml/scanner/Stop Scanner.yml
@@ -0,0 +1,32 @@
+info:
+ name: Stop Scanner
+ type: http
+ seq: 3
+http:
+ method: POST
+ url: '{{base_url}}/api/scanner/stop'
+ auth: inherit
+
+docs: |-
+ ## Stop Scanner
+
+ Stops the currently running media scanner service.
+
+ **Method:** POST
+
+ **Endpoint:** /api/scanner/stop
+
+ **Authentication:** Required (Bearer token)
+
+ **Response:**
+ - JSON object containing scanner status
+ - `status` (string): Scanner state ("stopped", "idle")
+ - `message` (string): Status message
+ - `stopped_at` (string): Timestamp when scanner stopped
+
+ **Status Codes:**
+ - 200: Scanner stopped successfully
+ - 401: Unauthorized
+ - 403: Forbidden (insufficient permissions)
+ - 409: Scanner not running
+ - 500: Internal server error
diff --git a/bruno-yaml/scanner/Stop Watch Mode.yml b/bruno-yaml/scanner/Stop Watch Mode.yml
new file mode 100644
index 0000000..6e9c65a
--- /dev/null
+++ b/bruno-yaml/scanner/Stop Watch Mode.yml
@@ -0,0 +1,32 @@
+info:
+ name: Stop Watch Mode
+ type: http
+ seq: 6
+http:
+ method: POST
+ url: '{{base_url}}/api/scanner/watch/stop'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"library_id\": \"{{library_id"
+
+docs: |-
+ ## Stop Watch Mode
+
+ Stops real-time file system monitoring for a specific library.
+
+ **Method:** POST
+
+ **Endpoint:** /api/scanner/watch/stop
+
+ **Authentication:** Required (Bearer token, Admin only)
+
+ **Request Body:**
+ - `library_id` (string, required): UUID of the library to stop watching
+ - Example: `"550e8400-e29b-41d4-a716-446655440000"`
+
+ **Response:** HTTP 200 (OK)
+ ```json
+ {
+ "message": "watch mode stopped for library",
+ "library_id": "550e8400-e29b-41d4-a716-446655440000"
diff --git a/bruno-yaml/sidecar/Download Device Sidecar File.yml b/bruno-yaml/sidecar/Download Device Sidecar File.yml
new file mode 100644
index 0000000..f8dedda
--- /dev/null
+++ b/bruno-yaml/sidecar/Download Device Sidecar File.yml
@@ -0,0 +1,22 @@
+info:
+ name: Download Device Sidecar File
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/{{device_id}}/sidecar/download'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{user_token
+
+docs: |-
+ ## Download Device Sidecar File
+
+ Downloads the sidecar configuration file for a device in JSON format.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/{device_id
diff --git a/bruno-yaml/sidecar/Get Device Sidecar Config.yml b/bruno-yaml/sidecar/Get Device Sidecar Config.yml
new file mode 100644
index 0000000..ca8d265
--- /dev/null
+++ b/bruno-yaml/sidecar/Get Device Sidecar Config.yml
@@ -0,0 +1,22 @@
+info:
+ name: Get Device Sidecar Config
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/devices/{{device_id}}/sidecar'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{user_token
+
+docs: |-
+ ## Get Device Sidecar Config
+
+ Retrieves sidecar configuration for a specific device.
+
+ **Method:** GET
+
+ **Endpoint:** /api/devices/{device_id
diff --git a/bruno-yaml/sidecar/Get System Configuration.yml b/bruno-yaml/sidecar/Get System Configuration.yml
new file mode 100644
index 0000000..2990bb8
--- /dev/null
+++ b/bruno-yaml/sidecar/Get System Configuration.yml
@@ -0,0 +1,36 @@
+info:
+ name: Get System Configuration
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/system/config'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{admin_token
+
+docs: |-
+ ## Get System Configuration
+
+ Retrieves system-wide configuration settings.
+
+ **Method:** GET
+
+ **Endpoint:** /api/system/config
+
+ **Authentication:** Bearer token (admin only)
+
+ **Response:**
+ - `base_url` (string): Base URL
+ - `opds_base_url` (string): OPDS endpoint URL
+ - `api_base_url` (string): API endpoint URL
+ - Additional configuration fields
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Forbidden (admin only)
+ - 500: Internal server error
diff --git a/bruno-yaml/sidecar/Update System Configuration.yml b/bruno-yaml/sidecar/Update System Configuration.yml
new file mode 100644
index 0000000..58cede6
--- /dev/null
+++ b/bruno-yaml/sidecar/Update System Configuration.yml
@@ -0,0 +1,42 @@
+info:
+ name: Update System Configuration
+ type: http
+ seq: 4
+http:
+ method: PUT
+ url: '{{base_url}}/api/system/config'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"base_url\": \"https://bookhoard.example.com\",\n \"opds_base_url\"\
+ : \"https://bookhoard.example.com/opds\",\n \"api_base_url\": \"https://bookhoard.example.com/api\""
+ headers:
+ - key: Authorization
+ value: Bearer {{admin_token
+
+docs: |-
+ ## Update System Configuration
+
+ Updates system-wide configuration settings for the Bookhoard instance.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/system/config
+
+ **Authentication:** Bearer token (admin only)
+
+ **Request Body:**
+ - `base_url` (string): Base URL for the instance
+ - `opds_base_url` (string): OPDS endpoint base URL
+ - `api_base_url` (string): API endpoint base URL
+
+ **Response:**
+ - `status` (string): Update status
+ - `config` (object): Updated configuration
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid configuration
+ - 401: Unauthorized
+ - 403: Forbidden (admin only)
+ - 500: Internal server error
diff --git a/bruno-yaml/system/get-scan-settings.yml b/bruno-yaml/system/get-scan-settings.yml
new file mode 100644
index 0000000..96ff129
--- /dev/null
+++ b/bruno-yaml/system/get-scan-settings.yml
@@ -0,0 +1,19 @@
+info:
+ name: Get System Scan Settings
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/libraries/scan-settings'
+ auth: inherit
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Get System Scan Settings
+
+ Retrieves current system-wide scan settings for all libraries.
+
+ **Authentication**: Admin token required
+ **Response**: Current scan frequency and auto-scan status
diff --git a/bruno-yaml/system/update-scan-settings.yml b/bruno-yaml/system/update-scan-settings.yml
new file mode 100644
index 0000000..806e5fc
--- /dev/null
+++ b/bruno-yaml/system/update-scan-settings.yml
@@ -0,0 +1,25 @@
+info:
+ name: Update System Scan Settings
+ type: http
+ seq: 2
+http:
+ method: PUT
+ url: '{{base_url}}/api/libraries/scan-settings'
+ auth: inherit
+ body:
+ type: json
+ json:
+ scan_frequency_minutes: 30
+ auto_scan_enabled: true
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Update System Scan Settings
+
+ Updates system-wide scan settings that apply to all libraries.
+
+ **Authentication**: Admin token required
+ **Request**: Scan frequency (15-1440 minutes) and enabled status
+ **Response**: Success message
diff --git a/bruno-yaml/universal-progress/Get Progress History.yml b/bruno-yaml/universal-progress/Get Progress History.yml
new file mode 100644
index 0000000..f476e7f
--- /dev/null
+++ b/bruno-yaml/universal-progress/Get Progress History.yml
@@ -0,0 +1,22 @@
+info:
+ name: Get Progress History
+ type: http
+ seq: 3
+http:
+ method: GET
+ url: '{{base_url}}/api/progress/{{mediaItemId}}/history'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{jwt
+
+docs: |-
+ ## Get Progress History
+
+ Retrieves historical reading progress sessions for a specific media item.
+
+ **Method:** GET
+
+ **Endpoint:** /api/progress/{mediaItemId
diff --git a/bruno-yaml/universal-progress/Get Universal Progress.yml b/bruno-yaml/universal-progress/Get Universal Progress.yml
new file mode 100644
index 0000000..acb1ba7
--- /dev/null
+++ b/bruno-yaml/universal-progress/Get Universal Progress.yml
@@ -0,0 +1,22 @@
+info:
+ name: Get Universal Progress
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/progress/{{mediaItemId}}'
+ auth: inherit
+ body:
+ type: none
+ headers:
+ - key: Authorization
+ value: Bearer {{jwt
+
+docs: |-
+ ## Get Universal Progress
+
+ Retrieves universal reading progress for a specific media item across all devices.
+
+ **Method:** GET
+
+ **Endpoint:** /api/progress/{mediaItemId
diff --git a/bruno-yaml/universal-progress/Update Universal Progress.yml b/bruno-yaml/universal-progress/Update Universal Progress.yml
new file mode 100644
index 0000000..d4b85a3
--- /dev/null
+++ b/bruno-yaml/universal-progress/Update Universal Progress.yml
@@ -0,0 +1,24 @@
+info:
+ name: Update Universal Progress
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/progress/{{mediaItemId}}'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"source\": \"web\",\n \"location\": {\n \"percentage\"\
+ : 0.45,\n \"page\": 90,\n \"total_pages\": 200"
+ headers:
+ - key: Authorization
+ value: Bearer {{jwt
+
+docs: |-
+ ## Update Universal Progress
+
+ Updates universal reading progress for a media item from a specific device or source.
+
+ **Method:** POST
+
+ **Endpoint:** /api/progress/{mediaItemId
diff --git a/bruno-yaml/user/admin/Delete Account.yml b/bruno-yaml/user/admin/Delete Account.yml
new file mode 100644
index 0000000..a76565b
--- /dev/null
+++ b/bruno-yaml/user/admin/Delete Account.yml
@@ -0,0 +1,25 @@
+info:
+ name: Delete Account
+ type: http
+ seq: 4
+http:
+ method: DELETE
+ url: '{{base_url}}/api/auth/account'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Delete Account
+
+ Permanently deletes user account and all associated data.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/auth/account
+
+ **Authentication:** Required
+
+ **Usage:**
+ - **Self-deletion**: DELETE /api/auth/account (no parameters)
+ - **Admin deletion**: DELETE /api/auth/account?user_id={uuid
diff --git a/bruno-yaml/user/admin/List Users.yml b/bruno-yaml/user/admin/List Users.yml
new file mode 100644
index 0000000..121bbb1
--- /dev/null
+++ b/bruno-yaml/user/admin/List Users.yml
@@ -0,0 +1,45 @@
+info:
+ name: List Users
+ type: http
+ seq: 5
+http:
+ method: GET
+ url: '{{base_url}}/api/auth/users'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## List Users
+
+ Retrieves a list of all users with complete user information.
+
+ **Method:** GET
+
+ **Endpoint:** /api/auth/users
+
+ **Authentication:** Required (Admin only)
+
+ **Response:** Array of user objects with complete information:
+ - `id` (string): User ID (UUID)
+ - `email` (string): Email address
+ - `username` (string): Username
+ - `first_name` (string): First name (empty if not set)
+ - `last_name` (string): Last name (empty if not set)
+ - `role` (string): User role ("user" or "admin")
+ - `theme` (string): Theme preference (empty if default)
+ - `max_devices` (integer): Maximum number of devices allowed
+ - `device_count` (integer): Current number of registered devices
+ - `created_at` (string): Creation timestamp (ISO 8601)
+ - `updated_at` (string): Last update timestamp (ISO 8601)
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
+ - 403: Forbidden (admin access required)
+
+ **Features:**
+ - Admin-only endpoint with complete user information
+ - Returns first_name, last_name, role, theme fields
+ - Includes device limits and current device count
+ - Useful for user management interfaces
diff --git a/bruno-yaml/user/admin/Register Admin User.yml b/bruno-yaml/user/admin/Register Admin User.yml
new file mode 100644
index 0000000..e3793fb
--- /dev/null
+++ b/bruno-yaml/user/admin/Register Admin User.yml
@@ -0,0 +1,58 @@
+info:
+ name: Register Admin User
+ type: http
+ seq: 4
+http:
+ method: POST
+ url: '{{base_url}}/api/auth/register'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"email\": \"maxdevices@example.com\",\n \"username\": \"\
+ maxdevicesuser\",\n \"password\": \"Test@Pass123!\",\n \"first_name\"\
+ : \"Test\",\n \"last_name\": \"User\",\n \"role\": \"admin\""
+
+docs: |-
+ ## Register Admin User
+
+ Creates a new admin user account with role-based restrictions.
+
+ **Method:** POST
+
+ **Endpoint:** /api/auth/register
+
+ **Request Body:**
+ - `email` (string): Email address
+ - `username` (string): Username
+ - `password` (string): Password
+ - `first_name` (string, optional): First name
+ - `last_name` (string, optional): Last name
+ - `role` (string): Must be "admin"
+
+ **Response:**
+ - `token` (string): JWT token with admin role
+ - `user` (object): User details
+ - `id` (string): User ID
+ - `email` (string): Email
+ - `username` (string): Username
+ - `theme` (string): User theme preference
+ - `first_name` (string, optional): First name
+ - `last_name` (string, optional): Last name
+ - `role` (string): User role ("admin")
+
+ **Status Codes:**
+ - 201: Created
+ - 400: Invalid input data
+ - 403: Forbidden - admin creation restrictions apply
+ - 409: User exists
+
+ **Role Restrictions:**
+ - **First User**: Anyone can create first admin (auto-assigned)
+ - **Existing Admins Present**: Only authenticated admins can create new admin accounts
+ - **Unauthenticated Users**: Cannot create admin accounts if any admin exists
+ - **Security**: Requires admin authentication for subsequent admin creation
+
+ **Usage Notes:**
+ - Use this request only when specifically creating admin accounts
+ - For regular user creation, use "Register User" request
+ - Admin token will have elevated privileges for administrative operations
diff --git a/bruno-yaml/user/admin/Update User Max Devices.yml b/bruno-yaml/user/admin/Update User Max Devices.yml
new file mode 100644
index 0000000..c251f11
--- /dev/null
+++ b/bruno-yaml/user/admin/Update User Max Devices.yml
@@ -0,0 +1,27 @@
+info:
+ name: Update User Max Devices
+ type: http
+ seq: 6
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/users/{{user_id}}/max-devices'
+ auth: inherit
+
+docs: |-
+ ## Update User Max Devices (Admin)
+
+ Updates the maximum number of devices a user can register.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/auth/users/:id/max-devices
+
+ **Authentication:** Required (Admin only)
+
+ **URL Parameters:**
+ - `id` (string): User ID (UUID)
+
+ **Request Body:**
+ ```json
+ {
+ "max_devices": 10
diff --git a/bruno-yaml/user/admin/scenarios/Delete User Account (Admin).yml b/bruno-yaml/user/admin/scenarios/Delete User Account (Admin).yml
new file mode 100644
index 0000000..43eb18e
--- /dev/null
+++ b/bruno-yaml/user/admin/scenarios/Delete User Account (Admin).yml
@@ -0,0 +1,19 @@
+info:
+ name: Delete User Account (Admin)
+ type: http
+ seq: 6
+http:
+ method: DELETE
+ url: '{{base_url}}/api/auth/account?user_id={{user_id}}'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Delete User Account (Admin)
+
+ Allows administrators to delete any user account by specifying user_id parameter.
+
+ **Method:** DELETE
+
+ **Endpoint:** /api/auth/account?user_id={user_id
diff --git a/bruno-yaml/user/admin/scenarios/Update User Max Devices - Invalid Too High.yml b/bruno-yaml/user/admin/scenarios/Update User Max Devices - Invalid Too High.yml
new file mode 100644
index 0000000..b974b20
--- /dev/null
+++ b/bruno-yaml/user/admin/scenarios/Update User Max Devices - Invalid Too High.yml
@@ -0,0 +1,16 @@
+info:
+ name: Update User Max Devices - Exceeds Maximum
+ type: http
+ seq: 3
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/users/{{user_id}}/max-devices'
+ auth: inherit
+
+docs: |-
+ ## Update User Max Devices - Invalid (Exceeds Maximum)
+
+ Attempts to set max_devices to 101 (above maximum of 100).
+
+ **Expected:** 400 Bad Request
+ **Response:** `{"error": "validation error"
diff --git a/bruno-yaml/user/admin/scenarios/Update User Max Devices - Invalid Zero.yml b/bruno-yaml/user/admin/scenarios/Update User Max Devices - Invalid Zero.yml
new file mode 100644
index 0000000..ac4acdb
--- /dev/null
+++ b/bruno-yaml/user/admin/scenarios/Update User Max Devices - Invalid Zero.yml
@@ -0,0 +1,16 @@
+info:
+ name: Update User Max Devices - Invalid Max Devices
+ type: http
+ seq: 2
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/users/{{user_id}}/max-devices'
+ auth: inherit
+
+docs: |-
+ ## Update User Max Devices - Invalid (Zero)
+
+ Attempts to set max_devices to 0 (below minimum).
+
+ **Expected:** 400 Bad Request
+ **Response:** `{"error": "validation error"
diff --git a/bruno-yaml/user/admin/scenarios/Update User Max Devices - Missing ID.yml b/bruno-yaml/user/admin/scenarios/Update User Max Devices - Missing ID.yml
new file mode 100644
index 0000000..98292c7
--- /dev/null
+++ b/bruno-yaml/user/admin/scenarios/Update User Max Devices - Missing ID.yml
@@ -0,0 +1,16 @@
+info:
+ name: Update User Max Devices - Missing ID
+ type: http
+ seq: 4
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/users//max-devices'
+ auth: inherit
+
+docs: |-
+ ## Update User Max Devices - Missing User ID
+
+ Attempts to update max devices without providing user ID.
+
+ **Expected:** 400 Bad Request
+ **Response:** `{"error": "user id required"
diff --git a/bruno-yaml/user/admin/scenarios/Update User Max Devices - Success.yml b/bruno-yaml/user/admin/scenarios/Update User Max Devices - Success.yml
new file mode 100644
index 0000000..462b904
--- /dev/null
+++ b/bruno-yaml/user/admin/scenarios/Update User Max Devices - Success.yml
@@ -0,0 +1,16 @@
+info:
+ name: Update User Max Devices - Success
+ type: http
+ seq: 1
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/users/{{user_id}}/max-devices'
+ auth: inherit
+
+docs: |-
+ ## Update User Max Devices - Success Case
+
+ Successfully updates a user's max devices limit to 5.
+
+ **Expected:** 200 OK
+ **Response:** `{"message": "max devices updated"
diff --git a/bruno-yaml/user/auth/Login User.yml b/bruno-yaml/user/auth/Login User.yml
new file mode 100644
index 0000000..f2e5e94
--- /dev/null
+++ b/bruno-yaml/user/auth/Login User.yml
@@ -0,0 +1,77 @@
+info:
+ name: Login User
+ type: http
+ seq: 1
+
+http:
+ method: POST
+ url: "{{base_url}}/api/auth/login"
+ body:
+ type: json
+ data: |-
+ {
+ "login": "testuser@example.com",
+ "password": "Test@Pass123!"
+ }
+
+runtime:
+ scripts:
+ - type: after-response
+ code: |-
+ function onResponse(res) {
+ try {
+ const responseBody = res.getBody();
+ const token = responseBody.access_token;
+ const refreshToken = responseBody.refresh_token;
+
+ if (token) {
+ bru.setEnvVar("token", token, { persist: true });
+ console.log("Access token saved:", token);
+ }
+
+ if (refreshToken) {
+ bru.setEnvVar("refresh_token", refreshToken, { persist: true });
+ console.log("Refresh token saved:", refreshToken);
+ }
+
+ if (!token && !refreshToken) {
+ console.log("No tokens found in response.");
+ }
+ } catch (error) {
+ console.error("Error in post-response script:", error.message);
+ }
+ }
+ onResponse(res);
+
+settings:
+ encodeUrl: true
+ timeout: 0
+ followRedirects: true
+ maxRedirects: 5
+
+docs: |-
+ ## Login User
+
+ Authenticates a user with email/username and password.
+
+ **Method:** POST
+
+ **Endpoint:** /api/auth/login
+
+ **Request Body:**
+ - `login` (string): Email or username
+ - `password` (string): Password
+
+ **Response:**
+ - `token` (string): JWT token
+ - `user` (object): User details
+ - `id` (string): User ID
+ - `email` (string): Email
+ - `username` (string): Username
+ - `theme` (string): User theme preference
+ - `first_name` (string): First name
+ - `last_name` (string): Last name
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Invalid credentials
diff --git a/bruno-yaml/user/auth/Logout User.yml b/bruno-yaml/user/auth/Logout User.yml
new file mode 100644
index 0000000..18e02d6
--- /dev/null
+++ b/bruno-yaml/user/auth/Logout User.yml
@@ -0,0 +1,40 @@
+info:
+ name: Logout User
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/auth/logout'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"refresh_token\": \"{{refresh_token"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Logout User
+
+ Logs out the user by revoking their refresh token. If no refresh token is provided, the request succeeds but no token is revoked.
+
+ **Method:** POST
+
+ **Endpoint:** /api/auth/logout
+
+ **Authentication:** Bearer token (optional)
+
+ **Request Body:**
+ - `refresh_token` (string, optional): Refresh token to revoke
+
+ **Response:**
+ - `message` (string): Success message
+
+ **Status Codes:**
+ - 200: Success - user logged out (token revoked if provided)
+ - 401: Unauthorized
+
+ **Example Response:**
+ ```json
+ {
+ "message": "logged out successfully"
diff --git a/bruno-yaml/user/auth/Refresh Token.yml b/bruno-yaml/user/auth/Refresh Token.yml
new file mode 100644
index 0000000..6a0a24f
--- /dev/null
+++ b/bruno-yaml/user/auth/Refresh Token.yml
@@ -0,0 +1,44 @@
+info:
+ name: Refresh Access Token
+ type: http
+ seq: 1
+http:
+ method: POST
+ url: '{{base_url}}/api/auth/refresh'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"refresh_token\": \"{{refresh_token"
+ headers:
+ - key: Content-Type
+ value: application/json
+
+docs: |-
+ ## Refresh Access Token
+
+ Refreshes an access token using a valid refresh token. Returns a new access token with 1-hour expiration.
+
+ **Method:** POST
+
+ **Endpoint:** /api/auth/refresh
+
+ **Authentication:** Not required (refresh token is in request body)
+
+ **Request Body:**
+ - `refresh_token` (string): Valid refresh token UUID
+
+ **Response:**
+ - `access_token` (string): New JWT access token (1 hour expiration)
+ - `token_type` (string): Token type (usually "Bearer")
+ - `expires_in` (number): Token lifetime in seconds (3600)
+
+ **Status Codes:**
+ - 200: Success - new access token generated
+ - 401: Unauthorized - invalid or expired refresh token
+
+ **Example Response (Success):**
+ ```json
+ {
+ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "token_type": "Bearer",
+ "expires_in": 3600
diff --git a/bruno-yaml/user/auth/Register User.yml b/bruno-yaml/user/auth/Register User.yml
new file mode 100644
index 0000000..0c75bec
--- /dev/null
+++ b/bruno-yaml/user/auth/Register User.yml
@@ -0,0 +1,57 @@
+info:
+ name: Register User
+ type: http
+ seq: 2
+http:
+ method: POST
+ url: '{{base_url}}/api/auth/register'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"email\": \"testuser@example.com\",\n \"username\": \"testuser\"\
+ ,\n \"password\": \"Test@Pass123!\",\n \"first_name\": \"Test\",\n \
+ \ \"last_name\": \"User\""
+
+docs: |-
+ ## Register User
+
+ Creates a new user account with role-based restrictions.
+
+ **Method:** POST
+
+ **Endpoint:** /api/auth/register
+
+ **Request Body:**
+ - `email` (string): Email address
+ - `username` (string): Username
+ - `password` (string): Password
+ - `first_name` (string, optional): First name
+ - `last_name` (string, optional): Last name
+ - `role` (string): User role ("user" or "admin")
+
+ **Response:**
+ - `token` (string): JWT token
+ - `user` (object): User details
+ - `id` (string): User ID
+ - `email` (string): Email
+ - `username` (string): Username
+ - `theme` (string): User theme preference
+ - `first_name` (string, optional): First name
+ - `last_name` (string, optional): Last name
+ - `role` (string): User role ("user" or "admin")
+
+ **Status Codes:**
+ - 201: Created
+ - 400: Invalid input data
+ - 403: Forbidden - role-based restrictions apply
+ - 409: User exists
+
+ **Role Restrictions:**
+ - **First User**: Automatically gets admin role regardless of request
+ - **Existing Admins Present**: Only authenticated admins can create new admin accounts
+ - **No Admins Yet**: Anyone can create first admin (auto-assigned)
+ - **Regular User Creation**: Anyone can create regular user accounts
+ - **Unauthenticated Users**: Can only create first admin, not subsequent admins
+
+ **Examples:**
+ - First admin creation: `{"email": "admin@example.com", "username": "admin", "password": "password123", "role": "admin"
diff --git a/bruno-yaml/user/profile/Get Profile.yml b/bruno-yaml/user/profile/Get Profile.yml
new file mode 100644
index 0000000..e89a8ed
--- /dev/null
+++ b/bruno-yaml/user/profile/Get Profile.yml
@@ -0,0 +1,33 @@
+info:
+ name: Get Profile
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: '{{base_url}}/api/auth/profile'
+ auth: inherit
+ body:
+ type: none
+
+docs: |-
+ ## Get User Profile
+
+ Retrieves the authenticated user's profile.
+
+ **Method:** GET
+
+ **Endpoint:** /api/auth/profile
+
+ **Authentication:** Required
+
+ **Response:**
+ - `id` (string): User ID
+ - `email` (string): Email
+ - `username` (string): Username
+ - `theme` (string): User theme preference
+ - `first_name` (string, optional): First name
+ - `last_name` (string, optional): Last name
+
+ **Status Codes:**
+ - 200: Success
+ - 401: Unauthorized
diff --git a/bruno-yaml/user/profile/Update Email.yml b/bruno-yaml/user/profile/Update Email.yml
new file mode 100644
index 0000000..456f5aa
--- /dev/null
+++ b/bruno-yaml/user/profile/Update Email.yml
@@ -0,0 +1,33 @@
+info:
+ name: Update Email
+ type: http
+ seq: 2
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/email'
+ auth: inherit
+ body:
+ type: json
+
+docs: |-
+ ## Update Email
+
+ Updates the authenticated user's email address.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/auth/email
+
+ **Authentication:** Required
+
+ **Request Body:**
+ - `email` (string, required): New email address (must be valid email format)
+
+ **Response:**
+ - `message` (string): Success message
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid email format
+ - 401: Unauthorized
+ - 409: Email already taken
diff --git a/bruno-yaml/user/profile/Update Password.yml b/bruno-yaml/user/profile/Update Password.yml
new file mode 100644
index 0000000..d1c2d88
--- /dev/null
+++ b/bruno-yaml/user/profile/Update Password.yml
@@ -0,0 +1,37 @@
+info:
+ name: Update Password
+ type: http
+ seq: 3
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/password'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"current_password\": \"password123\",\n \"new_password\"\
+ : \"newpassword123\",\n \"confirm_password\": \"newpassword123\""
+
+docs: |-
+ ## Update Password
+
+ Updates the authenticated user's password.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/auth/password
+
+ **Authentication:** Required
+
+ **Request Body:**
+ - `current_password` (string, required): Current password for verification
+ - `new_password` (string, required): New password (minimum 6 characters)
+ - `confirm_password` (string, required): Confirmation of new password
+
+ **Response:**
+ - `message` (string): Success message
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Password validation failed
+ - 401: Current password incorrect
+ - 401: Unauthorized
diff --git a/bruno-yaml/user/profile/Update Profile.yml b/bruno-yaml/user/profile/Update Profile.yml
new file mode 100644
index 0000000..0575185
--- /dev/null
+++ b/bruno-yaml/user/profile/Update Profile.yml
@@ -0,0 +1,34 @@
+info:
+ name: Update Profile
+ type: http
+ seq: 4
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/profile'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"first_name\": \"Updated\",\n \"last_name\": \"Name\""
+
+docs: |-
+ ## Update User Profile
+
+ Updates the authenticated user's profile information.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/auth/profile
+
+ **Authentication:** Required (Bearer token)
+
+ **Request Body:**
+ - `first_name` (string, optional): First name
+ - `last_name` (string, optional): Last name
+
+ **Response:**
+ - `message` (string): Success message
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid request
+ - 401: Unauthorized
diff --git a/bruno-yaml/user/profile/Update Theme.yml b/bruno-yaml/user/profile/Update Theme.yml
new file mode 100644
index 0000000..e6a81fb
--- /dev/null
+++ b/bruno-yaml/user/profile/Update Theme.yml
@@ -0,0 +1,33 @@
+info:
+ name: Update Theme
+ type: http
+ seq: 4
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/theme'
+ auth: inherit
+ body:
+ type: json
+ jsonBody: "{\n \"theme\": \"dracula\""
+
+docs: |-
+ ## Update User Theme
+
+ Updates the authenticated user's theme preference.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/auth/theme
+
+ **Authentication:** Required (Bearer token)
+
+ **Request Body:**
+ - `theme` (string): Theme name (tokyo-night, dracula, nord, solarized-dark, monokai, one-dark-pro, material-dark, catppuccin-mocha, catppuccin-macchiato, catppuccin-frappe, catppuccin-latte)
+
+ **Response:**
+ - `message` (string): Success message
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid theme
+ - 401: Unauthorized
diff --git a/bruno-yaml/user/profile/Update Username.yml b/bruno-yaml/user/profile/Update Username.yml
new file mode 100644
index 0000000..7ecfd39
--- /dev/null
+++ b/bruno-yaml/user/profile/Update Username.yml
@@ -0,0 +1,33 @@
+info:
+ name: Update Username
+ type: http
+ seq: 1
+http:
+ method: PUT
+ url: '{{base_url}}/api/auth/username'
+ auth: inherit
+ body:
+ type: json
+
+docs: |-
+ ## Update Username
+
+ Updates the authenticated user's username.
+
+ **Method:** PUT
+
+ **Endpoint:** /api/auth/username
+
+ **Authentication:** Required
+
+ **Request Body:**
+ - `username` (string, required): New username (3-50 characters)
+
+ **Response:**
+ - `message` (string): Success message
+
+ **Status Codes:**
+ - 200: Success
+ - 400: Invalid username
+ - 401: Unauthorized
+ - 409: Username already taken
diff --git a/bruno/admin/Get Admin Library.bru b/bruno/admin/Get Admin Library.bru
deleted file mode 100644
index e8924c1..0000000
--- a/bruno/admin/Get Admin Library.bru
+++ /dev/null
@@ -1,57 +0,0 @@
-meta {
- name: Get Admin Library
- type: http
- seq: 5
-}
-
-get {
- url: {{base_url}}/admin/library
- body: none
- auth: inherit
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() !== 200) {
- bru.testFailed("Expected status 200, got " + res.getStatus());
- return;
- }
-
- const contentType = res.getHeader("content-type");
- if (!contentType || !contentType.includes("text/html")) {
- bru.testFailed("Expected content-type to contain text/html, got " + contentType);
- return;
- }
-
- bru.testPassed("Admin library page returned successfully");
- }
- onResponse(res);
-
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Admin Library Page
-
- Retrieves the admin library page for administrative access.
-
- **Method:** GET
-
- **Endpoint:** /admin/library
-
- **Headers:**
- - `Authorization` (string): Bearer token
-
- **Response:**
- - HTML content for the admin library page
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden
-
-}
diff --git a/bruno/admin/Get Admin Profile.bru b/bruno/admin/Get Admin Profile.bru
deleted file mode 100644
index c8e4b14..0000000
--- a/bruno/admin/Get Admin Profile.bru
+++ /dev/null
@@ -1,46 +0,0 @@
-meta {
- name: Get Admin Profile
- type: http
- seq: 4
-}
-
-get {
- url: {{base_url}}/admin/profile
- body: none
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Admin Profile
-
- Retrieves the admin profile information for administrative access.
-
- **Method:** GET
-
- **Endpoint:** /admin/profile
-
- **Authentication:** Required (Bearer token)
-
- **Response:**
- - JSON object containing admin profile details
- - `id` (string): Admin user ID
- - `email` (string): Admin email
- - `username` (string): Admin username
- - `theme` (string): Theme preference
- - `first_name` (string): First name
- - `last_name` (string): Last name
- - `is_admin` (boolean): Admin status
- - `created_at` (string): Account creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (non-admin users)
- - 404: Profile not found
-}
diff --git a/bruno/analytics/Get Device Usage.bru b/bruno/analytics/Get Device Usage.bru
deleted file mode 100644
index 27ece3e..0000000
--- a/bruno/analytics/Get Device Usage.bru
+++ /dev/null
@@ -1,88 +0,0 @@
-meta {
- name: Get Device Usage
- type: http
- seq: 2
-}
-
-get {
- url: {{base_url}}/api/analytics/device-usage
- body: none
- auth: inherit
-}
-
-tests {
- test("status must be 200", function() {
- expect(res.status).to.eql(200);
- });
-
- test("response has devices array", function() {
- const body = JSON.parse(res.body);
- expect(body).to.have.property("devices");
- expect(body.devices).to.be.an("array");
- });
-
- test("devices have required fields", function() {
- const body = JSON.parse(res.body);
- if (body.devices.length > 0) {
- expect(body.devices[0]).to.have.property("device_name");
- expect(body.devices[0]).to.have.property("device_type");
- expect(body.devices[0]).to.have.property("sync_count");
- }
- });
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- Get usage statistics for all devices.
-
- **Endpoint**: GET /api/analytics/device-usage
- **Auth**: Required (Bearer token)
-
- ## Response Fields
-
- | Field | Type | Description |
- |-------|------|-------------|
- | devices | array | List of device usage statistics |
- | devices[].id | string | Device ID |
- | devices[].device_name | string | Device name |
- | devices[].device_type | string | Device type (kobo, kindle, koreader) |
- | devices[].sync_count | int | Number of sync operations |
- | devices[].last_sync | string | Last sync timestamp |
- | devices[].total_reading_minutes | int | Total reading time on device |
- | devices[].books_read | int | Number of books completed on device |
-
- ## Example Response
-
- ```json
- {
- "devices": [
- {
- "id": "789e4567-e89b-12d3-a456-426614174001",
- "device_name": "My Kobo Clara",
- "device_type": "kobo",
- "sync_count": 45,
- "last_sync": "2026-02-08T17:25:00Z",
- "total_reading_minutes": 1250,
- "books_read": 3
- }
- ]
- }
- ```
-
- ## Error Responses
-
- | Code | Description |
- |------|-------------|
- | 401 | Unauthorized |
- | 500 | Internal server error |
-
- ## Notes
-
- - Only shows devices registered to the authenticated user
- - Devices are sorted by sync_count in descending order
- - Includes both active and inactive devices
-}
diff --git a/bruno/analytics/Get Popular Books.bru b/bruno/analytics/Get Popular Books.bru
deleted file mode 100644
index 435cdc1..0000000
--- a/bruno/analytics/Get Popular Books.bru
+++ /dev/null
@@ -1,99 +0,0 @@
-meta {
- name: Get Popular Books
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/analytics/popular-books?limit=10
- body: none
- auth: inherit
-}
-
-tests {
- test("status must be 200", function() {
- expect(res.status).to.eql(200);
- });
-
- test("response has books array", function() {
- const body = JSON.parse(res.body);
- expect(body).to.have.property("books");
- expect(body.books).to.be.an("array");
- });
-
- test("books have required fields", function() {
- const body = JSON.parse(res.body);
- if (body.books.length > 0) {
- expect(body.books[0]).to.have.property("title");
- expect(body.books[0]).to.have.property("author");
- expect(body.books[0]).to.have.property("read_count");
- expect(body.books[0]).to.have.property("avg_completion");
- }
- });
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- Get popular books sorted by read count.
-
- **Endpoint**: GET /api/analytics/popular-books
- **Auth**: Required (Bearer token)
-
- ## Query Parameters
-
- | Parameter | Type | Required | Description |
- |-----------|------|-----------|-------------|
- | limit | int | No | Maximum number of books to return (default: 10) |
-
- ## Response Fields
-
- | Field | Type | Description |
- |-------|------|-------------|
- | books | array | List of popular books |
- | books[].media_item_id | string | Book ID |
- | books[].title | string | Book title |
- | books[].author | string | Book author |
- | books[].read_count | int | Number of times read |
- | books[].avg_completion | float | Average completion rate (0-1) |
- | books[].cover_url | string | Cover image URL |
-
- ## Example Request
-
- ```
- GET /api/analytics/popular-books?limit=10
- ```
-
- ## Example Response
-
- ```json
- {
- "books": [
- {
- "media_item_id": "323e4567-e89b-12d3-a456-426614174002",
- "title": "The Great Gatsby",
- "author": "F. Scott Fitzgerald",
- "read_count": 5,
- "avg_completion": 0.85,
- "cover_url": "/api/books/323e4567-e89b-12d3-a456-426614174002/cover"
- }
- ]
- }
- ```
-
- ## Error Responses
-
- | Code | Description |
- |------|-------------|
- | 401 | Unauthorized |
- | 500 | Internal server error |
-
- ## Notes
-
- - Books are sorted by read_count in descending order
- - Only books owned by the authenticated user are included
- - avg_completion is calculated from all reading sessions
-}
diff --git a/bruno/analytics/Get Reading Stats Date Range.bru b/bruno/analytics/Get Reading Stats Date Range.bru
deleted file mode 100644
index 6ec2bca..0000000
--- a/bruno/analytics/Get Reading Stats Date Range.bru
+++ /dev/null
@@ -1,95 +0,0 @@
-meta {
- name: Get Reading Stats Date Range
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31
- body: none
- auth: inherit
-}
-
-tests {
- test("status must be 200", function() {
- expect(res.status).to.eql(200);
- });
-
- test("response has daily_reading_minutes array", function() {
- const body = JSON.parse(res.body);
- expect(body).to.have.property("daily_reading_minutes");
- expect(body.daily_reading_minutes).to.be.an("array");
- });
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- Get reading statistics for a specific date range.
-
- **Endpoint**: GET /api/analytics/reading-stats
- **Auth**: Required (Bearer token)
-
- ## Query Parameters
-
- | Parameter | Type | Required | Description |
- |-----------|------|-----------|-------------|
- | start_date | string | No | Start date (ISO 8601 format, default: 30 days ago) |
- | end_date | string | No | End date (ISO 8601 format, default: today) |
-
- ## Response Fields
-
- | Field | Type | Description |
- |-------|------|-------------|
- | total_books_read | int | Total books completed in range |
- | total_pages_read | int | Total pages read in range |
- | total_reading_time_minutes | int | Total reading time in minutes |
- | completion_rate | float | Percentage of books completed (0-1) |
- | daily_reading_minutes | array | Daily reading time per day |
- | daily_reading_minutes[].date | string | Date (ISO 8601) |
- | daily_reading_minutes[].minutes | int | Minutes read on that date |
-
- ## Example Request
-
- ```
- GET /api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31
- ```
-
- ## Example Response
-
- ```json
- {
- "total_books_read": 2,
- "total_pages_read": 450,
- "total_reading_time_minutes": 720,
- "completion_rate": 0.85,
- "daily_reading_minutes": [
- {
- "date": "2024-01-01",
- "minutes": 30
- },
- {
- "date": "2024-01-02",
- "minutes": 45
- }
- ]
- }
- ```
-
- ## Error Responses
-
- | Code | Description |
- |------|-------------|
- | 400 | Invalid date format |
- | 401 | Unauthorized |
- | 500 | Internal server error |
-
- ## Notes
-
- - Dates must be in ISO 8601 format (YYYY-MM-DD)
- - The range is inclusive of both start and end dates
- - Daily data only includes days with reading activity > 0
-}
diff --git a/bruno/analytics/Get Reading Stats.bru b/bruno/analytics/Get Reading Stats.bru
deleted file mode 100644
index cc33640..0000000
--- a/bruno/analytics/Get Reading Stats.bru
+++ /dev/null
@@ -1,104 +0,0 @@
-meta {
- name: Get Reading Stats
- type: http
- seq: 4
-}
-
-get {
- url: {{base_url}}/api/analytics/reading-stats
- body: none
- auth: inherit
-}
-
-tests {
- test("status must be 200", function() {
- expect(res.status).to.eql(200);
- });
-
- test("response has required stats fields", function() {
- const body = JSON.parse(res.body);
- expect(body).to.have.property("total_books_read");
- expect(body).to.have.property("total_pages_read");
- expect(body).to.have.property("total_reading_time_minutes");
- expect(body).to.have.property("completion_rate");
- expect(body).to.have.property("daily_reading_minutes");
- });
-
- test("daily_reading_minutes is an array", function() {
- const body = JSON.parse(res.body);
- expect(body.daily_reading_minutes).to.be.an("array");
- });
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- Get overall reading statistics for the authenticated user.
-
- **Endpoint**: GET /api/analytics/reading-stats
- **Auth**: Required (Bearer token)
-
- ## Query Parameters
-
- | Parameter | Type | Required | Description |
- |-----------|------|-----------|-------------|
- | start_date | string | No | Start date (ISO 8601 format) |
- | end_date | string | No | End date (ISO 8601 format) |
-
- ## Response Fields
-
- | Field | Type | Description |
- |-------|------|-------------|
- | total_books_read | int | Total books completed |
- | total_pages_read | int | Total pages read |
- | total_reading_time_minutes | int | Total reading time in minutes |
- | completion_rate | float | Average book completion rate (0-1) |
- | daily_reading_minutes | array | Daily reading time breakdown |
- | daily_reading_minutes[].date | string | Date (ISO 8601) |
- | daily_reading_minutes[].minutes | int | Minutes read on that date |
-
- ## Example Request
-
- ```
- GET /api/analytics/reading-stats
- ```
-
- ## Example Response
-
- ```json
- {
- "total_books_read": 12,
- "total_pages_read": 3450,
- "total_reading_time_minutes": 5400,
- "completion_rate": 0.78,
- "daily_reading_minutes": [
- {
- "date": "2026-01-15",
- "minutes": 45
- },
- {
- "date": "2026-01-16",
- "minutes": 60
- }
- ]
- }
- ```
-
- ## Error Responses
-
- | Code | Description |
- |------|-------------|
- | 400 | Invalid date format |
- | 401 | Unauthorized |
- | 500 | Internal server error |
-
- ## Notes
-
- - Without date parameters, returns stats for the last 30 days
- - Dates must be in ISO 8601 format (YYYY-MM-DD) when provided
- - Only includes reading activity from the authenticated user
- - Daily data includes all days with reading activity
-}
diff --git a/bruno/books/Bulk Delete Books.bru b/bruno/books/Bulk Delete Books.bru
deleted file mode 100644
index 08aa3b1..0000000
--- a/bruno/books/Bulk Delete Books.bru
+++ /dev/null
@@ -1,85 +0,0 @@
-meta {
- name: Bulk Delete Media Items
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/media-items/bulk-delete
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "media_item_ids": [
- "{{bookId1}}",
- "{{bookId2}}",
- "{{bookId3}}"
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Bulk delete successful'] = true;
- const body = res.getBody();
- tests['Has results array'] = Array.isArray(body.results);
- tests['Has total count'] = body.total !== undefined;
- tests['Has deleted count'] = body.deleted !== undefined;
- tests['Has failed count'] = body.failed !== undefined;
- } else {
- tests['Bulk delete failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Delete Media Items
-
- Deletes multiple media items in a single request.
-
- **Method:** POST
-
- **Endpoint:** /api/media-items/bulk-delete
-
- **Authentication:** Required (Bearer token)
-
- **Request Body:**
- - `media_item_ids` (array of strings): Array of media item UUIDs to delete
-
- **Response:**
- - `results` (array): Results for each deletion attempt
- - `total` (number): Total number of media items processed
- - `deleted` (number): Number of successfully deleted media items
- - `failed` (number): Number of failed deletions
-
- **Status Codes:**
- - 200: Success (with partial results if some failed)
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden
- - 500: Internal server error
-
- **Example:**
- ```json
- {
- "media_item_ids": [
- "uuid-1",
- "uuid-2",
- "uuid-3"
- ]
- }
- ```
-}
diff --git a/bruno/books/Bulk Update Books.bru b/bruno/books/Bulk Update Books.bru
deleted file mode 100644
index fd0ea0a..0000000
--- a/bruno/books/Bulk Update Books.bru
+++ /dev/null
@@ -1,112 +0,0 @@
-meta {
- name: Bulk Update Media Items
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/media-items/bulk-update
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "media_item_updates": [
- {
- "media_item_id": "{{bookId1}}",
- "updates": {
- "title": "Updated Title",
- "genre": "Science Fiction",
- "tags": ["science fiction", "non-fiction", "ACME CORP."]
- }
- },
- {
- "media_item_id": "{{bookId2}}",
- "updates": {
- "author": "Updated Author",
- "contributors": ["O'Reilly Media", "Penguin Random House"]
- }
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Bulk update successful'] = true;
- const body = res.getBody();
- tests['Has results array'] = Array.isArray(body.results);
- tests['Has total count'] = body.total !== undefined;
- tests['Has updated count'] = body.updated !== undefined;
- } else {
- tests['Bulk update failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Update Media Items
-
- Updates multiple media items in a single request with different fields for each item.
-
- **Method:** POST
-
- **Endpoint:** /api/media-items/bulk-update
-
- **Authentication:** Required (Bearer token)
-
- **Request Body:**
- - `media_item_updates` (array): Array of update objects
- - `media_item_id` (string): Media item UUID to update
- - `updates` (object): Fields to update (can include title, author, genre, tags, contributors, etc.)
-
- **Response:**
- - `results` (array): Results for each update attempt
- - `total` (number): Total number of media items processed
- - `updated` (number): Number of successfully updated media items
- - `failed` (number): Number of failed updates
-
- **Status Codes:**
- - 200: Success (with partial results if some failed)
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden
- - 500: Internal server error
-
- **Example:**
- ```json
- {
- "media_item_updates": [
- {
- "media_item_id": "uuid-1",
- "updates": {
- "title": "New Title",
- "genre": "Fiction",
- "tags": ["fiction", "adventure"]
- }
- },
- {
- "media_item_id": "uuid-2",
- "updates": {
- "author": "Jane Doe",
- "contributors": ["Publisher Inc."]
- }
- }
- ]
- }
- ```
-
- **Note:** Each media item can have different fields updated. Only the specified fields are modified for each item.
-}
diff --git a/bruno/bruno.json b/bruno/bruno.json
deleted file mode 100644
index 2c4df5f..0000000
--- a/bruno/bruno.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "version": "1",
- "name": "Bookhoard API",
- "type": "collection",
- "docs": "API test collection for Bookhoard. Test credentials and data are documented in TEST_DATA.md to maintain alignment with Go integration tests."
-}
\ No newline at end of file
diff --git a/bruno/collection.bru b/bruno/collection.bru
deleted file mode 100644
index ed964c2..0000000
--- a/bruno/collection.bru
+++ /dev/null
@@ -1,184 +0,0 @@
-auth {
- mode: bearer
-}
-
-
-docs {
- # Bruno API Tests for Bookhoard
- This directory contains Bruno collection for testing the Bookhoard API with comprehensive REST documentation.
- Setup
- 1. Install Bruno: https://www.usebruno.com/
- 2. Open Bruno and import this collection folder
- 3. Select the "localhost" environment
- 4. Start the application with `podman-compose up --build` or `docker-compose up --build`
- 5. Register/Login first, then use Bearer token for protected endpoints
- Available Tests
- Authentication (Public & Private)
- - **Register User**: POST /api/auth/register - Create new account with role-based restrictions
- - **Login User**: POST /api/auth/login - Authenticate (email or username)
- - **Refresh Token**: POST /api/auth/refresh - Get new access token
- - **Logout**: POST /api/auth/logout - Invalidate refresh token
- User Profile Management
- - **Get Profile**: GET /api/auth/profile - Get current user info
- - **Update Profile**: PUT /api/auth/profile - Update first_name, last_name
- - **Update Email**: PUT /api/auth/email - Update email address
- - **Update Username**: PUT /api/auth/username - Update username
- - **Update Password**: PUT /api/auth/password - Update password
- - **Update Theme**: PUT /api/auth/theme - Update theme preference
- Admin User Management
- - **List Users**: GET /api/auth/users - Get all users with complete info (admin only)
- - **Delete Account**: DELETE /api/auth/account - Delete own account or admin deletes other accounts
- Libraries (Admin Only)
- - **Create Library**: POST /api/libraries - Create new library (Ebooks, Comics, Manga)
- - **Get Libraries**: GET /api/libraries - List all libraries (admin)
- - **Get Library**: GET /api/libraries/:id - Get library details
- - **Update Library**: PUT /api/libraries/:id - Update library settings
- - **Delete Library**: DELETE /api/libraries/:id - Remove library
- - **Add Library Folder**: POST /api/libraries/:id/folders - Add scanning folder
- - **Get Library Folders**: GET /api/libraries/:id/folders - List folders
- - **Delete Library Folder**: DELETE /api/libraries/:id/folders/:folder_id - Remove folder
- - **Get Library Stats**: GET /api/libraries/:id/stats - Library statistics
- - **Get Library Types**: GET /api/libraries/types - Available library types
- Media Items (Mixed Access)
- - **List Media Items**: GET /api/media-items - Paginated list (filter/sort by library, author, series, etc.)
- - **Get Media Item**: GET /api/media-items/:id - Single item details (all users)
- - **Create Media Item**: POST /api/media-items - Add new item (admin only)
- - **Update Media Item**: PUT /api/media-items/:id - Modify metadata (admin only)
- - **Delete Media Item**: DELETE /api/media-items/:id - Remove item (admin only)
- - **Filter Media Items**: POST /api/media-items/filter - Advanced filtering
- - **EPUB Download**: GET /api/media-items/:id/download - Download EPUB file
- - **Cover Image**: GET /api/media-items/:id/cover - Get cover image
- Reading Progress (All Users)
- - **Get Progress**: GET /api/progress/:media_id - User's reading progress for media item
- - **Update Progress**: PUT /api/progress/:media_id - Update reading progress
- - **Get Device Progress**: GET /api/progress/device/:device_id - Progress by device
- Universal Progress (All Users)
- - **Get Universal Progress**: GET /api/universal-progress/:sha256 - Get progress by book hash
- - **Update Universal Progress**: PUT /api/universal-progress - Update universal progress
- Notes (All Users)
- - **Get Notes**: GET /api/notes/:media_id - Get notes for media item
- - **Create Note**: POST /api/notes - Add new note
- - **Update Note**: PUT /api/notes/:id - Update note content
- - **Delete Note**: DELETE /api/notes/:id - Remove note
- Highlights (All Users)
- - **Get Highlights**: GET /api/highlights/:media_id - Get highlights for media item
- - **Create Highlight**: POST /api/highlights - Add new highlight
- - **Update Highlight**: PUT /api/highlights/:id - Update highlight
- - **Delete Highlight**: DELETE /api/highlights/:id - Remove highlight
- Ratings (All Users)
- - **Get Rating**: GET /api/ratings/:media_id - User's rating (returns 0 if unrated)
- - **Create/Update Rating**: POST /api/ratings - Rate media item (1-5 stars, half-star precision)
- - **Delete Rating**: DELETE /api/ratings/:media_id - Remove rating
- Collections (All Users)
- - **List Collections**: GET /api/collections - Get user's collections
- - **Get Collection**: GET /api/collections/:id - Collection details with media items
- - **Create Collection**: POST /api/collections - Create new collection
- - **Update Collection**: PUT /api/collections/:id - Update collection
- - **Delete Collection**: DELETE /api/collections/:id - Remove collection
- - **Add Auto-Assign Rule**: POST /api/collections/:id/rules - Add automatic rule
- - **Remove Auto-Assign Rule**: DELETE /api/collections/:id/rules/:rule_id - Remove rule
- - **Test Rule**: POST /api/collections/:id/rules/test - Preview rule matches
- - **Bulk Assign**: POST /api/collections/:id/assign - Manually add media items
- Device Management (All Users)
- - **Register Device**: POST /api/devices/register - Register new device
- - **List Devices**: GET /api/devices - Get user's devices
- - **Get Device**: GET /api/devices/:id - Device details
- - **Delete Device**: DELETE /api/devices/:id - Unregister device
- - **Sync Device**: POST /api/devices/:id/sync - Trigger device sync
- Sync Protocols (Device Integration)
- - **KOReader Sync**: POST /api/sync/koreader - KOReader progress/notes/highlights sync
- - **Kobo Sync**: POST /api/sync/kobo - Kobo progress/notes/highlights sync
- Scanner (Admin Only)
- - **Scan Libraries**: POST /api/scanner/scan - Scan library folders
- - **Start Scanner**: POST /api/scanner/start - Start real-time monitoring
- - **Stop Scanner**: POST /api/scanner/stop - Stop monitoring
- - **Get Scan Settings**: GET /api/scanner/settings - Scan configuration
- Analytics (Admin Only)
- - **Get Analytics**: GET /api/analytics - Usage statistics and metrics
- Book Matching (All Users)
- - **Search Books**: GET /api/book-matching/search - Search by ISBN, title, author
- - **Link Book**: POST /api/book-matching/link - Link media item to external database
- OPDS (All Users)
- - **OPDS Feeds**: GET /opds/* - OPDS catalog for e-reader integration
- - **OPDS Acquisition**: GET /opds/acquisition/* - Download media items
- WebSocket (Real-time)
- - **WebSocket**: WS /api/ws - Real-time sync events (progress, notes, highlights)
- Collection Organization
- bruno/
- ├── user/ # User authentication and profile
- │ ├── auth/ # Login, register, refresh
- │ ├── profile/ # Profile management
- │ └── admin/ # User administration (admin only)
- ├── library/ # Library management
- │ ├── Create/Update/Delete Libraries
- │ ├── Library Folders
- │ ├── Library Stats
- │ └── Scan Settings
- ├── media-items/ # Media item operations
- │ ├── List/Get/Create/Update/Delete
- │ ├── Filter and Sort
- │ ├── Download EPUB
- │ ├── Cover Images
- │ └── Ratings
- ├── progress/ # Reading progress tracking
- ├── universal-progress/ # Cross-device universal progress
- ├── notes/ # User notes
- ├── highlights/ # Book highlights
- ├── collections/ # Smart collections
- ├── devices/ # Device registration
- ├── sync-koreader/ # KOReader sync protocol
- ├── sync-kobo/ # Kobo sync protocol
- ├── scanner/ # Library scanning
- ├── analytics/ # Usage statistics
- ├── books/ # Book matching/linking
- ├── kobo/ # Kobo-specific operations
- ├── koreader/ # KOReader-specific operations
- ├── opds/ # OPDS catalog feeds
- └── admin/ # Admin operations
- ## Security Features
- ### Registration Restrictions
- - **First User**: Automatically gets admin role regardless of request
- - **Existing Admins**: Only authenticated admins can create new admin accounts
- - **Regular Users**: Anyone can create regular user accounts
- - **Unauthenticated**: Can only create first admin, not subsequent admins
- ### User Management
- - **Self-Deletion**: Users can delete their own accounts
- - **Admin Override**: Admins can delete any user account
- - **Last Admin Protection**: Cannot delete the last admin account in the system
- ### Role System
- - **Admin**: Full access - manage libraries, media items, users, scanner
- - **User**: Read access - view media items, create collections, track progress, rate, annotate
- ### Device Authentication
- - **No Passwords**: Devices use QR code registration and access tokens
- - **User Approval**: Device registration requires user approval via web interface
- ### JWT Tokens
- - **Access Token**: Valid for 1 hour, sent via Bearer header
- - **Refresh Token**: Valid for 7 days, used to get new access tokens
- ### Rate Limiting
- - **Auth Endpoints**: 10 requests/minute per IP
- ### Data Isolation
- - **Progress, Notes, Highlights, Ratings**: User-specific
- - **Collections**: User-specific (admins see all users' collections)
- - **Devices**: User-specific
- ## Documentation Features
- Each request includes:
- - **Detailed descriptions** of functionality
- - **Parameter specifications** (required/optional, types)
- - **Request/Response examples**
- - **Error response codes** and meanings
- - **Authentication requirements**
- ## Notes
- - **Authentication Flow**: Register → Login → Use Bearer token for all other requests
- - **Media Items vs Books**: The API uses "media items" (supports ebooks, comics, manga)
- - **Library System**: Organized by libraries (Ebooks, Comics, Manga) with scanning folders
- - **Universal Progress**: Cross-device sync using SHA-256 book hashes
- - **Smart Collections**: Auto-assign rules based on genre, author, series, tags, etc.
- - **OPDS Support**: Wireless book delivery to e-readers (Kobo, KOReader)
- - **Device Protocols**: Native sync for KOReader and Kobo devices
- - **Rating System**: Half-star precision (1-10 scale internally, displayed as 1-5 stars)
- - **Admin Setup**: First admin must be created by updating user role in database
- - **Variables**: Update collection variables for testing (media_id, library_id, device_id, etc.)
- - **Security**: Passwords hashed with bcrypt, unique email/username constraints, role-based access control
- - **JSON**: All requests/responses use JSON format
- - **WebSocket**: Real-time events for sync updates across devices
-}
diff --git a/bruno/collections/Add Books to Collection.bru b/bruno/collections/Add Books to Collection.bru
deleted file mode 100644
index 2cbd082..0000000
--- a/bruno/collections/Add Books to Collection.bru
+++ /dev/null
@@ -1,22 +0,0 @@
-meta {
- name: Add Books to Collection
- type: http
- seq: 6
-}
-
-post {
- url: {{base_url}}/api/collections/{{collection_id}}/books
- body: json
- auth: inherit
-}
-
-
-body:json {
- {
- "book_ids": [
- "{{book_id_1}}",
- "{{book_id_2}}",
- "{{book_id_3}}"
- ]
- }
-}
diff --git a/bruno/collections/Bulk Add Books to Collections.bru b/bruno/collections/Bulk Add Books to Collections.bru
deleted file mode 100644
index 59b4235..0000000
--- a/bruno/collections/Bulk Add Books to Collections.bru
+++ /dev/null
@@ -1,103 +0,0 @@
-meta {
- name: Bulk Add Books to Collections
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/collections/bulk-add-books
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "operations": [
- {
- "collection_id": "{{collectionId1}}",
- "book_ids": [
- "{{bookId1}}",
- "{{bookId2}}"
- ]
- },
- {
- "collection_id": "{{collectionId2}}",
- "book_ids": [
- "{{bookId3}}"
- ]
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Bulk add successful'] = true;
- const body = res.getBody();
- tests['Has results array'] = Array.isArray(body.results);
- tests['All operations processed'] = body.results.length > 0;
- } else {
- tests['Bulk add failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Add Books to Collections
-
- Adds multiple books to multiple collections in a single request. Each operation specifies a collection and a list of books to add.
-
- **Method:** POST
-
- **Endpoint:** /api/collections/bulk-add-books
-
- **Authentication:** Bearer token
-
- **Request Body:**
- - `operations` (array): Array of collection-book operations
- - `collection_id` (string): Collection UUID
- - `book_ids` (array): Array of book UUIDs to add to the collection
-
- **Response:**
- - `results` (array): Results for each operation
- - `total` (number): Total number of operations
- - `success` (number): Number of successful operations
- - `failed` (number): Number of failed operations
-
- **Status Codes:**
- - 200: Success (with partial results if some failed)
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden
- - 500: Internal server error
-
- **Example:**
- ```json
- {
- "operations": [
- {
- "collection_id": "collection-uuid-1",
- "book_ids": ["book-1", "book-2"]
- },
- {
- "collection_id": "collection-uuid-2",
- "book_ids": ["book-3"]
- }
- ]
- }
- ```
-
- **Note:** Adding a book that's already in a collection is idempotent (no error).
-}
diff --git a/bruno/collections/Bulk Remove Books - All Books.bru b/bruno/collections/Bulk Remove Books - All Books.bru
deleted file mode 100644
index 1696c47..0000000
--- a/bruno/collections/Bulk Remove Books - All Books.bru
+++ /dev/null
@@ -1,100 +0,0 @@
-meta {
- name: Bulk Remove Books - All Books
- type: http
- seq: 5
-}
-
-post {
- url: {{base_url}}/api/collections/{{collection_id}}/books/bulk-remove
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "book_ids": [
- "{{bookId1}}",
- "{{bookId2}}",
- "{{bookId3}}"
- ]
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Remove Books from Collection
-
- Removes multiple books from a collection in a single request.
-
- **Method:** POST
-
- **Endpoint:** /api/collections/{collection_id}/books/bulk-remove
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `collection_id` (string): Collection UUID
-
- **Request Body:**
- - `book_ids` (array): Array of book UUIDs to remove from the collection
-
- **Response:**
- - `removed` (number): Number of books successfully removed
- - `total` (number): Total number of books processed
- - `results` (array): Results for each removal attempt
- - `book_id` (string): Book UUID
- - `success` (boolean): Whether the removal succeeded
- - `error` (string, optional): Error message if failed
-
- **Status Codes:**
- - 200: Success (with partial results if some failed)
- - 400: Invalid request data (e.g., empty book_ids array)
- - 401: Unauthorized
- - 403: Forbidden
- - 404: Collection not found
- - 500: Internal server error
-
- **Example Request:**
- ```json
- {
- "book_ids": [
- "book-uuid-1",
- "book-uuid-2",
- "book-uuid-3"
- ]
- }
- ```
-
- **Example Response:**
- ```json
- {
- "removed": 2,
- "total": 3,
- "results": [
- {
- "book_id": "book-uuid-1",
- "success": true
- },
- {
- "book_id": "book-uuid-2",
- "success": true
- },
- {
- "book_id": "book-uuid-3",
- "success": false,
- "error": "Book not in collection"
- }
- ]
- }
- ```
-
- **Note:** Removing a book that's not in the collection returns success: false for that book but doesn't fail the entire request. Empty book_ids array returns 400.
-}
diff --git a/bruno/collections/Bulk Remove Books - Empty List.bru b/bruno/collections/Bulk Remove Books - Empty List.bru
deleted file mode 100644
index 3c4f0f8..0000000
--- a/bruno/collections/Bulk Remove Books - Empty List.bru
+++ /dev/null
@@ -1,46 +0,0 @@
-meta {
- name: Bulk Remove Books - Empty List
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/collections/{{collection_id}}/books/bulk-remove
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "book_ids": []
- }
-}
-
-script:post-response {
- function onResponse(res) {
- tests['Returns 400 for empty list'] = res.getStatus() === 400;
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Remove Books - Empty List Validation
-
- Tests validation behavior when providing an empty book_ids array.
-
- **Expected Result:** 400 Bad Request
-
- **Validation Rule:** book_ids array must contain at least one book UUID.
-
- **Purpose:** Ensures the API properly validates input and rejects empty removal requests.
-}
diff --git a/bruno/collections/Bulk Remove Books - Invalid IDs.bru b/bruno/collections/Bulk Remove Books - Invalid IDs.bru
deleted file mode 100644
index 25676a4..0000000
--- a/bruno/collections/Bulk Remove Books - Invalid IDs.bru
+++ /dev/null
@@ -1,59 +0,0 @@
-meta {
- name: Bulk Remove Books - Invalid IDs
- type: http
- seq: 4
-}
-
-post {
- url: {{base_url}}/api/collections/{{collection_id}}/books/bulk-remove
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "book_ids": [
- "{{bookId1}}",
- "invalid-uuid-format",
- "{{bookId2}}"
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Partial success accepted'] = true;
- const body = res.getBody();
- tests['Has removed count'] = body.removed !== undefined;
- tests['Has results array'] = Array.isArray(body.results);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Remove Books - Invalid IDs
-
- Tests behavior when the book_ids array contains invalid UUID formats or non-existent books.
-
- **Expected Result:** 200 OK with partial success
-
- **Purpose:** Verifies that:
- - Invalid UUID formats don't crash the endpoint
- - Non-existent book IDs are handled gracefully
- - Valid IDs in the same request are still processed
- - Response includes detailed results showing which succeeded/failed
-
- **Note:** The endpoint should process all valid IDs and report failures for invalid ones, allowing clients to handle partial failures appropriately.
-}
diff --git a/bruno/collections/Bulk Remove Books - Single Book.bru b/bruno/collections/Bulk Remove Books - Single Book.bru
deleted file mode 100644
index 997dee7..0000000
--- a/bruno/collections/Bulk Remove Books - Single Book.bru
+++ /dev/null
@@ -1,52 +0,0 @@
-meta {
- name: Bulk Remove Books - Single Book
- type: http
- seq: 3
-}
-
-post {
- url: {{base_url}}/api/collections/{{collection_id}}/books/bulk-remove
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "book_ids": [
- "{{bookId1}}"
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests['Single book removed'] = body.removed === 1;
- tests['Total is 1'] = body.total === 1;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Remove Books - Single Book
-
- Tests that bulk remove endpoint works correctly with a single book.
-
- **Expected Result:** 200 OK with removed: 1, total: 1
-
- **Purpose:** Verifies the bulk remove endpoint handles single-item arrays correctly, providing flexibility for clients to use the same endpoint for both single and multiple removals.
-
- **Note:** Using bulk remove for a single book is functionally equivalent to the single remove endpoint but allows for consistent error handling and response format.
-}
diff --git a/bruno/collections/Create Collection.bru b/bruno/collections/Create Collection.bru
deleted file mode 100644
index ae52209..0000000
--- a/bruno/collections/Create Collection.bru
+++ /dev/null
@@ -1,34 +0,0 @@
-meta {
- name: Create Collection
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/collections
- body: json
- auth: inherit
-}
-
-
-body:json {
- {
- "name": "Science Fiction",
- "description": "My favorite sci-fi books",
- "color": "#ff0000",
- "icon": "🚀",
- "auto_assign_rules": [
- {
- "id": "rule-1",
- "field": "genre",
- "operator": "equals",
- "value": "Science Fiction",
- "priority": 8
- }
- ],
- "view_settings": {
- "sort_by": "title",
- "view_mode": "grid"
- }
- }
-}
diff --git a/bruno/collections/Create Device Mapping.bru b/bruno/collections/Create Device Mapping.bru
deleted file mode 100644
index f41e98d..0000000
--- a/bruno/collections/Create Device Mapping.bru
+++ /dev/null
@@ -1,22 +0,0 @@
-meta {
- name: Create Device Mapping
- type: http
- seq: 9
-}
-
-post {
- url: {{base_url}}/api/devices/{{device_id}}/collections
- body: json
- auth: inherit
-}
-
- token: {{token}}
-}
-
-body:json {
- {
- "collection_id": "{{collection_id}}",
- "device_shelf_name": "Sci-Fi",
- "sync_direction": "bidirectional"
- }
-}
diff --git a/bruno/collections/Delete Collection.bru b/bruno/collections/Delete Collection.bru
deleted file mode 100644
index c6a6f70..0000000
--- a/bruno/collections/Delete Collection.bru
+++ /dev/null
@@ -1,10 +0,0 @@
-meta {
- name: Delete Collection
- type: http
- seq: 5
-}
-
-delete {
- url: {{base_url}}/api/collections/{{collection_id}}
- auth: inherit
-}
diff --git a/bruno/collections/Delete Device Mapping.bru b/bruno/collections/Delete Device Mapping.bru
deleted file mode 100644
index fbf42f7..0000000
--- a/bruno/collections/Delete Device Mapping.bru
+++ /dev/null
@@ -1,10 +0,0 @@
-meta {
- name: Delete Device Mapping
- type: http
- seq: 11
-}
-
-delete {
- url: {{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}}
- auth: inherit
-}
diff --git a/bruno/collections/Get Book Collections.bru b/bruno/collections/Get Book Collections.bru
deleted file mode 100644
index 7b5e730..0000000
--- a/bruno/collections/Get Book Collections.bru
+++ /dev/null
@@ -1,10 +0,0 @@
-meta {
- name: Get Book Collections
- type: http
- seq: 12
-}
-
-get {
- url: {{base_url}}/api/collections/books/{{book_id}}
- auth: inherit
-}
diff --git a/bruno/collections/Get Collection.bru b/bruno/collections/Get Collection.bru
deleted file mode 100644
index 477b301..0000000
--- a/bruno/collections/Get Collection.bru
+++ /dev/null
@@ -1,10 +0,0 @@
-meta {
- name: Get Collection
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/collections/{{collection_id}}
- auth: inherit
-}
diff --git a/bruno/collections/Get Collections.bru b/bruno/collections/Get Collections.bru
deleted file mode 100644
index 4b90abd..0000000
--- a/bruno/collections/Get Collections.bru
+++ /dev/null
@@ -1,10 +0,0 @@
-meta {
- name: Get Collections
- type: http
- seq: 2
-}
-
-get {
- url: {{base_url}}/api/collections?include_auto=true&sort_by=name
- auth: inherit
-}
diff --git a/bruno/collections/Get Device Mappings.bru b/bruno/collections/Get Device Mappings.bru
deleted file mode 100644
index de2c587..0000000
--- a/bruno/collections/Get Device Mappings.bru
+++ /dev/null
@@ -1,10 +0,0 @@
-meta {
- name: Get Device Mappings
- type: http
- seq: 8
-}
-
-get {
- url: {{base_url}}/api/devices/{{device_id}}/collections
- auth: inherit
-}
diff --git a/bruno/collections/Remove Book from Collection.bru b/bruno/collections/Remove Book from Collection.bru
deleted file mode 100644
index d20967b..0000000
--- a/bruno/collections/Remove Book from Collection.bru
+++ /dev/null
@@ -1,10 +0,0 @@
-meta {
- name: Remove Book from Collection
- type: http
- seq: 7
-}
-
-delete {
- url: {{base_url}}/api/collections/{{collection_id}}/books/{{book_id}}
- auth: inherit
-}
diff --git a/bruno/collections/Test Collection Rules - Author Contains.bru b/bruno/collections/Test Collection Rules - Author Contains.bru
deleted file mode 100644
index b0c74fb..0000000
--- a/bruno/collections/Test Collection Rules - Author Contains.bru
+++ /dev/null
@@ -1,61 +0,0 @@
-meta {
- name: Test Collection Rules - Author Contains
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/collections/test-rules
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "rules": [
- {
- "field": "author",
- "operator": "contains",
- "value": "Asimov"
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Author search successful'] = true;
- const body = res.getBody();
- tests['Has matches array'] = Array.isArray(body.matches);
- tests['Found books by Asimov'] = body.matches.length > 0;
- } else {
- tests['Author search failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Test Collection Rules - Author Contains
-
- Tests the "contains" operator on the author field to find books by a specific author (partial match).
-
- **Example Use Case:** Finding all books by an author whose name contains "Asimov" (e.g., "Isaac Asimov").
-
- **Operator:** `contains` - Matches if the field contains the specified value as a substring (case-insensitive typically).
-
- **Expected Result:** Returns all books where the author field contains "Asimov".
-
- **Purpose:** Demonstrates text-based partial matching for author searches, useful when you don't need the exact author name or want to find books by authors with similar names.
-}
diff --git a/bruno/collections/Test Collection Rules - Copyright Year Greater Than.bru b/bruno/collections/Test Collection Rules - Copyright Year Greater Than.bru
deleted file mode 100644
index c636bc7..0000000
--- a/bruno/collections/Test Collection Rules - Copyright Year Greater Than.bru
+++ /dev/null
@@ -1,63 +0,0 @@
-meta {
- name: Test Collection Rules - Copyright Year Greater Than
- type: http
- seq: 3
-}
-
-post {
- url: {{base_url}}/api/collections/test-rules
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "rules": [
- {
- "field": "copyright_year",
- "operator": "greater_than",
- "value": "2000"
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Year comparison successful'] = true;
- const body = res.getBody();
- tests['Has matches array'] = Array.isArray(body.matches);
- tests('Found books after 2000', body.matches.length >= 0);
- } else {
- tests['Year comparison failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Test Collection Rules - Copyright Year Greater Than
-
- Tests the "greater_than" operator on the copyright_year field to find books published after a specific year.
-
- **Example Use Case:** Creating a "Modern Books" collection with books published after 2000.
-
- **Operator:** `greater_than` - Matches if the field value is greater than the specified value (numeric comparison).
-
- **Field:** `copyright_year` - The year the book was copyrighted/published.
-
- **Expected Result:** Returns all books with copyright_year greater than 2000 (i.e., published in 2001 or later).
-
- **Purpose:** Demonstrates numeric comparison operators for creating date-based collections, useful for organizing books by publication era.
-}
diff --git a/bruno/collections/Test Collection Rules - Empty Rules Array.bru b/bruno/collections/Test Collection Rules - Empty Rules Array.bru
deleted file mode 100644
index 9ea1c02..0000000
--- a/bruno/collections/Test Collection Rules - Empty Rules Array.bru
+++ /dev/null
@@ -1,48 +0,0 @@
-meta {
- name: Test Collection Rules - Empty Rules Array
- type: http
- seq: 5
-}
-
-post {
- url: {{base_url}}/api/collections/test-rules
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "rules": []
- }
-}
-
-script:post-response {
- function onResponse(res) {
- tests['Empty rules rejected'] = res.getStatus() === 400;
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Test Collection Rules - Empty Rules Array
-
- Tests validation behavior when providing an empty rules array.
-
- **Expected Result:** 400 Bad Request
-
- **Validation Rule:** rules array must contain at least one rule object.
-
- **Purpose:** Ensures the API properly validates input and rejects empty rule sets, preventing accidental queries that would return all books or cause performance issues.
-
- **Use Case:** Client-side validation should prevent sending empty rules, but the API should also validate to catch malformed requests.
-}
diff --git a/bruno/collections/Test Collection Rules - No Matches.bru b/bruno/collections/Test Collection Rules - No Matches.bru
deleted file mode 100644
index 791e7a2..0000000
--- a/bruno/collections/Test Collection Rules - No Matches.bru
+++ /dev/null
@@ -1,63 +0,0 @@
-meta {
- name: Test Collection Rules - No Matches
- type: http
- seq: 4
-}
-
-post {
- url: {{base_url}}/api/collections/test-rules
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "rules": [
- {
- "field": "genre",
- "operator": "equals",
- "value": "NonExistentGenre123456"
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests['No matches returned'] = body.total === 0;
- tests['Empty matches array'] = body.matches.length === 0;
- tests['Success with zero results'] = true;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Test Collection Rules - No Matches
-
- Tests behavior when collection rules don't match any books in the library.
-
- **Example Use Case:** Validating that a new genre name doesn't exist before creating a collection for it, or testing edge cases.
-
- **Expected Result:** 200 OK with empty matches array and total: 0
-
- **Purpose:** Verifies that the API handles zero-match scenarios gracefully:
- - Returns 200 (success) not 404
- - Returns empty array, not null
- - Returns total: 0 for clarity
- - No errors thrown for no results
-
- **Note:** An empty result set is a valid response and doesn't indicate an error. This allows users to test rules confidently before creating collections.
-}
diff --git a/bruno/collections/Test Collection Rules.bru b/bruno/collections/Test Collection Rules.bru
deleted file mode 100644
index 365215f..0000000
--- a/bruno/collections/Test Collection Rules.bru
+++ /dev/null
@@ -1,119 +0,0 @@
-meta {
- name: Test Collection Rules
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/collections/test-rules
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "rules": [
- {
- "field": "genre",
- "operator": "equals",
- "value": "Science Fiction"
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Rules test successful'] = true;
- const body = res.getBody();
- tests['Has matches array'] = Array.isArray(body.matches);
- tests['Has total count'] = body.total !== undefined;
- } else {
- tests['Rules test failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Test Collection Rules
-
- Tests collection rules against the library to see which books match, without creating a collection. Useful for previewing what books would be included in a collection with specific rules.
-
- **Method:** POST
-
- **Endpoint:** /api/collections/test-rules
-
- **Authentication:** Bearer token
-
- **Request Body:**
- - `rules` (array): Array of rule objects to test
- - `field` (string): Field to test (genre, author, copyright_year, tags, etc.)
- - `operator` (string): Comparison operator
- - `equals`: Exact match
- - `contains`: Contains substring (for text fields)
- - `greater_than`: Greater than (for numeric fields)
- - `less_than`: Less than (for numeric fields)
- - `not_equals`: Not equal to
- - `starts_with`: Starts with
- - `ends_with`: Ends with
- - `is_empty`: Field is empty or null
- - `is_not_empty`: Field is not empty and not null
- - `value` (string): Value to compare against (not required for is_empty/is_not_empty)
-
- **Response:**
- - `matches` (array): Array of matching books
- - `id` (string): Book UUID
- - `title` (string): Book title
- - `author` (string): Book author
- - `genre` (string): Book genre
- - Additional book metadata
- - `total` (number): Total number of matching books
-
- **Status Codes:**
- - 200: Success - returns matching books
- - 400: Invalid request (empty rules array, invalid field/operator)
- - 401: Unauthorized
- - 500: Internal server error
-
- **Example Request:**
- ```json
- {
- "rules": [
- {
- "field": "genre",
- "operator": "equals",
- "value": "Science Fiction"
- }
- ]
- }
- ```
-
- **Example Response:**
- ```json
- {
- "matches": [
- {
- "id": "book-uuid-1",
- "title": "Foundation",
- "author": "Isaac Asimov",
- "genre": "Science Fiction"
- }
- ],
- "total": 1
- }
- ```
-
- **Note:** This endpoint is useful for validating collection rules before creating a collection, or for dynamically querying books based on criteria.
-}
diff --git a/bruno/collections/Update Collection.bru b/bruno/collections/Update Collection.bru
deleted file mode 100644
index 2a21855..0000000
--- a/bruno/collections/Update Collection.bru
+++ /dev/null
@@ -1,34 +0,0 @@
-meta {
- name: Update Collection
- type: http
- seq: 4
-}
-
-put {
- url: {{base_url}}/api/collections/{{collection_id}}
- body: json
- auth: inherit
-}
-
-
-body:json {
- {
- "name": "Sci-Fi Favorites",
- "description": "Updated description",
- "color": "#00ff00",
- "icon": "⭐",
- "auto_assign_rules": [
- {
- "id": "rule-2",
- "field": "series",
- "operator": "equals",
- "value": "Foundation",
- "priority": 9
- }
- ],
- "view_settings": {
- "sort_by": "author",
- "view_mode": "list"
- }
- }
-}
diff --git a/bruno/collections/Update Device Mapping.bru b/bruno/collections/Update Device Mapping.bru
deleted file mode 100644
index 973ccda..0000000
--- a/bruno/collections/Update Device Mapping.bru
+++ /dev/null
@@ -1,19 +0,0 @@
-meta {
- name: Update Device Mapping
- type: http
- seq: 10
-}
-
-put {
- url: {{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}}
- body: json
- auth: inherit
-}
-
-
-body:json {
- {
- "device_shelf_name": "Science Fiction",
- "sync_direction": "book_to_device"
- }
-}
diff --git a/bruno/conflicts/Bulk Dismiss Conflicts.bru b/bruno/conflicts/Bulk Dismiss Conflicts.bru
deleted file mode 100644
index 2f182ff..0000000
--- a/bruno/conflicts/Bulk Dismiss Conflicts.bru
+++ /dev/null
@@ -1,84 +0,0 @@
-meta {
- name: Bulk Dismiss Conflicts
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/conflicts/bulk-dismiss
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "conflict_ids": [
- "{{conflictId1}}",
- "{{conflictId2}}"
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Bulk dismiss successful'] = true;
- const body = res.getBody();
- tests['Has results array'] = Array.isArray(body.results);
- tests['Has total count'] = body.total !== undefined;
- tests['Has success count'] = body.success !== undefined;
- tests['Has failed count'] = body.failed !== undefined;
- } else {
- tests['Bulk dismiss failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Dismiss Conflicts
-
- Dismisses multiple sync conflicts without resolving them. This removes them from the conflict list while leaving the data unchanged.
-
- **Method:** POST
-
- **Endpoint:** /api/conflicts/bulk-dismiss
-
- **Authentication:** Bearer token
-
- **Request Body:**
- - `conflict_ids` (array): Array of conflict UUIDs to dismiss
-
- **Response:**
- - `results` (array): Results for each dismissal
- - `total` (number): Total number of conflicts processed
- - `success` (number): Number of successfully dismissed conflicts
- - `failed` (number): Number of failed dismissals
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden
- - 404: One or more conflicts not found
- - 500: Internal server error
-
- **Example:**
- ```json
- {
- "conflict_ids": ["uuid-1", "uuid-2"]
- }
- ```
-
- **Note:** Dismissing a conflict removes it from the conflict list but does not merge or resolve the conflicting data. Use this when you want to ignore a conflict and handle it manually.
-}
diff --git a/bruno/conflicts/Bulk Resolve Conflicts.bru b/bruno/conflicts/Bulk Resolve Conflicts.bru
deleted file mode 100644
index 61a38db..0000000
--- a/bruno/conflicts/Bulk Resolve Conflicts.bru
+++ /dev/null
@@ -1,93 +0,0 @@
-meta {
- name: Bulk Resolve Conflicts
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/conflicts/bulk-resolve
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "conflict_ids": [
- "{{conflictId1}}",
- "{{conflictId2}}",
- "{{conflictId3}}"
- ],
- "strategy": "most_recent"
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Bulk resolve successful'] = true;
- const body = res.getBody();
- tests['Has results array'] = Array.isArray(body.results);
- tests['Has total count'] = body.total !== undefined;
- tests['Has success count'] = body.success !== undefined;
- tests['Has failed count'] = body.failed !== undefined;
- tests['Total equals sum of success and failed'] = body.total === body.success + body.failed;
- } else {
- tests['Bulk resolve failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Resolve Conflicts
-
- Resolves multiple sync conflicts in a single request using a specified resolution strategy.
-
- **Method:** POST
-
- **Endpoint:** /api/conflicts/bulk-resolve
-
- **Authentication:** Bearer token
-
- **Request Body:**
- - `conflict_ids` (array): Array of conflict UUIDs to resolve
- - `strategy` (string): Resolution strategy
- - `most_recent`: Use the most recently updated progress
- - `highest_progress`: Use the reading progress with the highest percent read
- - `server`: Always prefer server-side data
- - `device`: Always prefer device-side data
-
- **Response:**
- - `results` (array): Results for each conflict resolution
- - `total` (number): Total number of conflicts processed
- - `success` (number): Number of successfully resolved conflicts
- - `failed` (number): Number of failed resolutions
-
- **Status Codes:**
- - 200: Success (with partial results if some failed)
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden
- - 404: One or more conflicts not found
- - 500: Internal server error
-
- **Example:**
- ```json
- {
- "conflict_ids": ["uuid-1", "uuid-2", "uuid-3"],
- "strategy": "most_recent"
- }
- ```
-
- **Note:** Conflicts are resolved atomically per conflict. If one resolution fails, others may still succeed.
-}
diff --git a/bruno/conflicts/Bulk Resolve Highest Progress.bru b/bruno/conflicts/Bulk Resolve Highest Progress.bru
deleted file mode 100644
index 47beac1..0000000
--- a/bruno/conflicts/Bulk Resolve Highest Progress.bru
+++ /dev/null
@@ -1,74 +0,0 @@
-meta {
- name: Bulk Resolve with Highest Progress Strategy
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/conflicts/bulk-resolve
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "conflict_ids": [
- "{{conflictId1}}"
- ],
- "strategy": "highest_progress"
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- tests['Highest progress strategy successful'] = true;
- const body = res.getBody();
- tests['At least one conflict resolved'] = body.success > 0;
- } else {
- tests['Strategy failed'] = false;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Resolve with Highest Progress Strategy
-
- Resolves multiple sync conflicts using the "highest_progress" strategy, which keeps the reading progress with the highest percentage read.
-
- **Method:** POST
-
- **Endpoint:** /api/conflicts/bulk-resolve
-
- **Authentication:** Bearer token
-
- **Request Body:**
- - `conflict_ids` (array): Array of conflict UUIDs to resolve
- - `strategy` (string): Must be "highest_progress"
-
- **Response:**
- - `results` (array): Results for each conflict resolution
- - `total` (number): Total number of conflicts processed
- - `success` (number): Number of successfully resolved conflicts
- - `failed` (number): Number of failed resolutions
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden
- - 500: Internal server error
-
- **Note:** The highest progress strategy is ideal when you want to preserve the most reading progress across devices. Use this when you've been reading on multiple devices and want to keep the furthest position.
-}
diff --git a/bruno/conflicts/Delete Conflict.bru b/bruno/conflicts/Delete Conflict.bru
deleted file mode 100644
index 13dd972..0000000
--- a/bruno/conflicts/Delete Conflict.bru
+++ /dev/null
@@ -1,50 +0,0 @@
-meta {
- name: Delete Conflict
- type: http
- seq: 4
-}
-
-delete {
- url: {{base_url}}/api/conflicts/{{conflict_id}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Conflict
-
- Permanently deletes a specific conflict record from the system.
-
- **Method:** DELETE
-
- **Endpoint:** /api/conflicts/{conflict_id}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `conflict_id` (string): Conflict UUID to delete
-
- **Response:** 204 No Content on success
-
- **Status Codes:**
- - 204: Success - conflict deleted
- - 401: Unauthorized
- - 404: Conflict not found
- - 500: Internal server error
-
- **Use Cases:**
- - Conflict was created in error
- - Dismissing a conflict without resolving it
- - Conflict is no longer relevant (e.g., book deleted)
-
- **Note:** This permanently removes the conflict record with no undo option. Consider resolving the conflict instead if you want to maintain an audit trail of what happened.
-}
diff --git a/bruno/conflicts/Dismiss All Resolved.bru b/bruno/conflicts/Dismiss All Resolved.bru
deleted file mode 100644
index 5895386..0000000
--- a/bruno/conflicts/Dismiss All Resolved.bru
+++ /dev/null
@@ -1,54 +0,0 @@
-meta {
- name: Dismiss All Resolved Conflicts
- type: http
- seq: 5
-}
-
-post {
- url: {{base_url}}/api/conflicts/dismiss-all
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Dismiss All Resolved Conflicts
-
- Deletes all resolved conflicts for the authenticated user, cleaning up the conflict list.
-
- **Method:** POST
-
- **Endpoint:** /api/conflicts/dismiss-all
-
- **Authentication:** Bearer token
-
- **Response:**
- - `deleted` (number): Number of conflict records that were deleted
-
- **Status Codes:**
- - 200: Success - conflicts deleted
- - 401: Unauthorized
- - 500: Internal server error
-
- **Example Response:**
- ```json
- {
- "deleted": 5
- }
- ```
-
- **Use Cases:**
- - Clean up conflicts list after reviewing resolutions
- - Remove old resolved conflicts no longer needed
- - Maintain a clean conflict history
-
- **Note:** Only conflicts with status "user_resolved" or "auto_resolved" are deleted. Unresolved conflicts are preserved.
-}
diff --git a/bruno/conflicts/Get Conflict Details.bru b/bruno/conflicts/Get Conflict Details.bru
deleted file mode 100644
index 74f3f1c..0000000
--- a/bruno/conflicts/Get Conflict Details.bru
+++ /dev/null
@@ -1,93 +0,0 @@
-meta {
- name: Get Conflict Details
- type: http
- seq: 2
-}
-
-get {
- url: {{base_url}}/api/conflicts/{{conflict_id}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Conflict Details
-
- Retrieves detailed information about a specific conflict, including side-by-side comparison of conflicting data from all sources.
-
- **Method:** GET
-
- **Endpoint:** /api/conflicts/{conflict_id}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `conflict_id` (string): Conflict UUID
-
- **Response:**
- - `id` (string): Conflict UUID
- - `media_item_id` (string): Associated book UUID
- - `media_item_title` (string): Book title
- - `conflict_type` (string): Type of conflict (progress, note, highlight)
- - `conflict_data` (object): Side-by-side comparison from each source
- - Each source includes:
- - `source` (string): Device/source identifier (koreader, kobo, web, etc.)
- - `timestamp` (string): When this data was recorded
- - `data` (object): The conflicting data
- - `percentage` (number): Reading progress
- - `epubcfi` (string): EPUB location
- - `chapter` (number): Chapter number
- - `resolution_status` (string): Current status (unresolved, user_resolved, auto_resolved)
- - `resolution_data` (object, optional): If resolved, includes resolution details
- - `resolved_by` (string, optional): User ID who resolved it
- - `resolved_at` (string, optional): When it was resolved
- - `created_at` (string): When conflict was detected
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Conflict not found
- - 500: Internal server error
-
- **Example Response:**
- ```json
- {
- "id": "conflict-uuid",
- "media_item_id": "book-uuid",
- "media_item_title": "Foundation",
- "conflict_type": "progress",
- "conflict_data": {
- "koreader": {
- "source": "koreader",
- "timestamp": "2026-01-30T20:10:00Z",
- "data": {
- "percentage": 0.45,
- "epubcfi": "epubcfi(/6/4/2:15)",
- "chapter": 3
- }
- },
- "kobo": {
- "source": "kobo",
- "timestamp": "2026-01-30T20:05:00Z",
- "data": {
- "percentage": 0.42,
- "location": "unknown"
- }
- }
- },
- "resolution_status": "unresolved",
- "created_at": "2026-01-30T20:10:00Z"
- }
- ```
-
- **Note:** Use this to get full details before resolving, showing exactly what data differs between sources.
-}
diff --git a/bruno/conflicts/List Conflicts.bru b/bruno/conflicts/List Conflicts.bru
deleted file mode 100644
index 9616215..0000000
--- a/bruno/conflicts/List Conflicts.bru
+++ /dev/null
@@ -1,94 +0,0 @@
-meta {
- name: List Conflicts
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/conflicts?status=unresolved
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## List Conflicts
-
- Lists all sync conflicts for the authenticated user with optional filtering.
-
- **Method:** GET
-
- **Endpoint:** /api/conflicts
-
- **Authentication:** Bearer token
-
- **Query Parameters:**
- - `status` (string, optional): Filter by resolution status
- - `unresolved`: Only unresolved conflicts (default)
- - `user_resolved`: Conflicts resolved by user
- - `auto_resolved`: Automatically resolved conflicts
- - `all`: All conflicts regardless of status
- - `type` (string, optional): Filter by conflict type
- - `progress`: Reading progress conflicts
- - `note`: Bookmark/note conflicts
- - `highlight`: Highlight conflicts
-
- **Response:**
- - `conflicts` (array): Array of conflict objects
- - `total` (number): Total number of conflicts matching filters
- - `unresolved` (number): Number of unresolved conflicts
-
- **Each Conflict Object:**
- - `id` (string): Conflict UUID
- - `media_item_id` (string): Associated book UUID
- - `media_item_title` (string): Book title
- - `conflict_type` (string): Type of conflict (progress, note, highlight)
- - `conflict_data` (object): Side-by-side comparison of conflicting data
- - `koreader`: Data from KOReader device
- - `kobo`: Data from Kobo device
- - `web`: Data from web interface
- - `resolution_status` (string): Current status (unresolved, user_resolved, auto_resolved)
- - `created_at` (string): ISO 8601 timestamp when conflict was detected
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-
- **Example Request:**
- ```
- GET /api/conflicts?status=unresolved&type=progress
- ```
-
- **Example Response:**
- ```json
- {
- "conflicts": [
- {
- "id": "conflict-uuid",
- "media_item_id": "book-uuid",
- "media_item_title": "Foundation",
- "conflict_type": "progress",
- "conflict_data": {
- "koreader": { "percentage": 0.65, "epubcfi": "..." },
- "kobo": { "percentage": 0.43, "epubcfi": "..." }
- },
- "resolution_status": "unresolved",
- "created_at": "2026-01-31T12:00:00Z"
- }
- ],
- "total": 1,
- "unresolved": 1
- }
- ```
-
- **Note:** Conflicts occur when multiple devices update the same book data without syncing first.
-}
diff --git a/bruno/conflicts/Resolve Conflict.bru b/bruno/conflicts/Resolve Conflict.bru
deleted file mode 100644
index dd2bb76..0000000
--- a/bruno/conflicts/Resolve Conflict.bru
+++ /dev/null
@@ -1,103 +0,0 @@
-meta {
- name: Resolve Conflict
- type: http
- seq: 3
-}
-
-post {
- url: {{base_url}}/api/conflicts/{{conflict_id}}/resolve
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "winner": "koreader",
- "manual_data": null,
- "apply_to_all_future_conflicts": false,
- "reason": "User chose more recent progress"
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Resolve Conflict
-
- Resolves a sync conflict by choosing which source to use for the conflicting data.
-
- **Method:** POST
-
- **Endpoint:** /api/conflicts/{conflict_id}/resolve
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `conflict_id` (string): Conflict UUID
-
- **Request Body:**
- - `winner` (string): Source to choose
- - `koreader`: Use KOReader device data
- - `kobo`: Use Kobo device data
- - `web`: Use web interface data
- - `manual`: Use custom merged data (requires manual_data)
- - `manual_data` (object, optional): Required if winner is "manual"
- - `percentage` (number): Reading progress percentage (0-1)
- - `epubcfi` (string): EPUB Canonical Fragment Identifier
- - `chapter` (number, optional): Chapter number
- - `page` (number, optional): Page number
- - `apply_to_all_future_conflicts` (boolean): Auto-resolve future conflicts from this source
- - `reason` (string, optional): Explanation for the resolution choice
-
- **Response:**
- - `conflict_resolved` (boolean): True if successful
- - `applied_to` (string): What was updated (progress, annotations, etc.)
- - `devices_synced` (array): List of device IDs that were notified
-
- **Status Codes:**
- - 200: Success - conflict resolved
- - 400: Invalid request data
- - 401: Unauthorized
- - 404: Conflict not found
- - 500: Internal server error
-
- **Example - Choose KOReader:**
- ```json
- {
- "winner": "koreader",
- "manual_data": null,
- "apply_to_all_future_conflicts": false,
- "reason": "More recent progress"
- }
- ```
-
- **Example - Manual Override:**
- ```json
- {
- "winner": "manual",
- "manual_data": {
- "percentage": 0.43,
- "epubcfi": "epubcfi(/6/4/2:20)",
- "chapter": 3
- },
- "apply_to_all_future_conflicts": false,
- "reason": "Custom merged position"
- }
- ```
-
- **After Resolution:**
- - Winning data is applied to reading progress
- - All connected devices notified via WebSocket
- - Conflict status changes to "user_resolved"
- - Resolution stored for audit trail
-
- **Note:** Manual override allows precise control when automatic resolution doesn't capture the correct state.
-}
diff --git a/bruno/devices/Add Books to Kobo Shelf.bru b/bruno/devices/Add Books to Kobo Shelf.bru
deleted file mode 100644
index 71511b1..0000000
--- a/bruno/devices/Add Books to Kobo Shelf.bru
+++ /dev/null
@@ -1,65 +0,0 @@
-meta {
- name: Add Books to Kobo Shelf
- type: http
- seq: 1
-}
-
-post {
- url: {{baseURL}}/api/devices/{{deviceID}}/shelves
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{userToken}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "media_item_ids": [
- "{{bookUUID1}}",
- "{{bookUUID2}}"
- ],
- "shelf_name": "Reading List",
- "shelf_position": 0
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Add Books to Kobo Shelf
-
- Add one or more books to a Kobo device shelf. Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization.
-
- **Method:** POST
-
- **Endpoint:** /api/devices/{deviceID}/shelves
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `deviceID` (string): Device UUID
-
- **Request Body:**
- - `media_item_ids` (array): Array of book UUIDs to add
- - `shelf_name` (string): Name of the shelf (e.g., "Reading List", "Favorites")
- - `shelf_position` (number, optional): Position on the shelf (default: 0)
-
- **Response:**
- - `message` (string): Success message
- - `added_count` (number): Number of books added
-
- **Status Codes:**
- - 200: Success - books added to shelf
- - 400: Invalid request data
- - 401: Unauthorized
- - 404: Device or one or more books not found
- - 500: Internal server error
-
- **Note:** Adding a book that's already on the shelf updates its position if a new position is specified. Supports multiple shelves for organizing content on the Kobo device.
-}
diff --git a/bruno/devices/Approve Device Registration.bru b/bruno/devices/Approve Device Registration.bru
deleted file mode 100644
index a47f591..0000000
--- a/bruno/devices/Approve Device Registration.bru
+++ /dev/null
@@ -1,44 +0,0 @@
-meta {
- name: Approve Device Registration
- type: http
- seq: 6
-}
-
-get {
- url: {{base_url}}/api/devices/approve/{{registration_id}}
- body: none
- auth: inherit
-}
-
-docs {
- ## Approve Device Registration
-
- Approves a pending device registration request.
-
- **Method:** GET
-
- **Endpoint:** /api/devices/approve/:registration_id
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `registration_id` (string): Registration request UUID
-
- **Response:**
- - Success message with approved device details
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Registration not found
- - 400: Invalid registration status
-
- **Example Response:**
- ```json
- {
- "message": "Device registration approved",
- "device_id": "uuid",
- "device_name": "My Kobo"
- }
- ```
-}
diff --git a/bruno/devices/Check Registration Status.bru b/bruno/devices/Check Registration Status.bru
deleted file mode 100644
index bb4fcf3..0000000
--- a/bruno/devices/Check Registration Status.bru
+++ /dev/null
@@ -1,17 +0,0 @@
-meta {
- name: Check Registration Status
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/devices/register/status
- body: json
- auth: none
-}
-
-body:json {
- {
- "registration_id": "{{registrationId}}"
- }
-}
diff --git a/bruno/devices/Clear Kobo Shelf.bru b/bruno/devices/Clear Kobo Shelf.bru
deleted file mode 100644
index eb70009..0000000
--- a/bruno/devices/Clear Kobo Shelf.bru
+++ /dev/null
@@ -1,50 +0,0 @@
-meta {
- name: Clear Kobo Shelf
- type: http
- seq: 1
-}
-
-delete {
- url: {{baseURL}}/api/devices/{{deviceID}}/shelves/clear?shelf={{shelfName}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{userToken}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Clear Kobo Shelf
-
- Clear all books from a Kobo device shelf, or all shelves if no shelf name is specified.
-
- **Method:** DELETE
-
- **Endpoint:** /api/devices/{deviceID}//shelves/clear?shelf={shelfName}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `deviceID` (string): Device UUID
-
- **Query Parameters:**
- - `shelf` (string, optional): Shelf name to clear. If omitted, clears all shelves.
-
- **Response:**
- - `message` (string): Success message
- - `cleared_count` (number): Number of books removed from shelf
-
- **Status Codes:**
- - 200: Success - shelf cleared
- - 401: Unauthorized
- - 404: Device not found
- - 500: Internal server error
-
- **Note:** Removes all books from the specified shelf. If no shelf name is provided, clears all shelves for the device. This operation cannot be undone.
-}
diff --git a/bruno/devices/Create Device File Alias.bru b/bruno/devices/Create Device File Alias.bru
deleted file mode 100644
index 658d967..0000000
--- a/bruno/devices/Create Device File Alias.bru
+++ /dev/null
@@ -1,97 +0,0 @@
-meta {
- name: Create Device File Alias
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/devices/{{device_id}}/file-aliases
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "file_path": "/mnt/sd/books/my-book.kepub.epub",
- "media_item_id": "{{media_item_id}}"
- }
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_create_device_file_alias_success(status, headers, body) {
- if (status !== 201 && status !== 200) {
- throw new Error("Expected status 201 or 200, got " + status);
- }
-
- const contentType = headers["content-type"];
- if (!contentType || !contentType.includes("application/json")) {
- throw new Error("Expected content-type to contain application/json");
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data.id) {
- throw new Error("Response missing id field");
- }
-
- if (!data.file_path) {
- throw new Error("Response missing file_path field");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- deviceId: "8821b703-1234-5678-9123-446655440002"
- mediaItemId: "8821b703-1234-5678-9123-446655440001"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Create Device File Alias
-
- Creates a new file alias for a device. File aliases map device-specific file paths to media items.
-
- **Method:** POST
-
- **Endpoint:** /api/devices/:id/file-aliases
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string, required): Device UUID
-
- **Request Body:**
- - `file_path` (string, required): Device-specific file path
- - `media_item_id` (string, required): Media item UUID to link to
-
- **Response:** Created file alias object
- - `id` (string): Alias UUID
- - `device_id` (string): Device UUID
- - `file_path` (string): Device-specific file path
- - `media_item_id` (string): Associated media item UUID
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 201: Created
- - 200: Success
- - 400: Invalid request body
- - 401: Unauthorized
- - 404: Device not found
- - 500: Internal server error
-}
diff --git a/bruno/devices/Delete Device.bru b/bruno/devices/Delete Device.bru
deleted file mode 100644
index 83bbfb8..0000000
--- a/bruno/devices/Delete Device.bru
+++ /dev/null
@@ -1,45 +0,0 @@
-meta {
- name: Delete Device
- type: http
- seq: 6
-}
-
-delete {
- url: {{base_url}}/api/devices/{{deviceId}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Device
-
- Deletes a device and unregisters it from the user's account.
-
- **Method:** DELETE
-
- **Endpoint:** /api/devices/{deviceId}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `deviceId` (string): Device UUID
-
- **Response:** 204 No Content on success
-
- **Status Codes:**
- - 204: Success - device deleted
- - 401: Unauthorized
- - 404: Device not found
- - 500: Internal server error
-
- **Note:** This action cannot be undone. All device data and sync history will be removed.
-}
diff --git a/bruno/devices/Get Device File Aliases.bru b/bruno/devices/Get Device File Aliases.bru
deleted file mode 100644
index b0657dd..0000000
--- a/bruno/devices/Get Device File Aliases.bru
+++ /dev/null
@@ -1,78 +0,0 @@
-meta {
- name: Get Device File Aliases
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/devices/{{device_id}}/file-aliases
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_get_device_file_aliases_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- const contentType = headers["content-type"];
- if (!contentType || !contentType.includes("application/json")) {
- throw new Error("Expected content-type to contain application/json");
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!Array.isArray(data)) {
- throw new Error("Expected response body to be an array");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- deviceId: "8821b703-1234-5678-9123-446655440002"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Device File Aliases
-
- Retrieves all file aliases for a specific device. File aliases are used to map device-specific file paths to media items.
-
- **Method:** GET
-
- **Endpoint:** /api/devices/:id/file-aliases
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string, required): Device UUID
-
- **Response:** Array of file alias objects
- - `id` (string): Alias UUID
- - `device_id` (string): Device UUID
- - `file_path` (string): Device-specific file path
- - `media_item_id` (string): Associated media item UUID
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Device not found
- - 500: Internal server error
-}
diff --git a/bruno/devices/Get Device.bru b/bruno/devices/Get Device.bru
deleted file mode 100644
index 375d94b..0000000
--- a/bruno/devices/Get Device.bru
+++ /dev/null
@@ -1,50 +0,0 @@
-meta {
- name: Get Device
- type: http
- seq: 4
-}
-
-get {
- url: {{base_url}}/api/devices/{{deviceId}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Device
-
- Retrieves detailed information about a specific device.
-
- **Method:** GET
-
- **Endpoint:** /api/devices/{deviceId}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `deviceId` (string): Device UUID
-
- **Response:**
- - `id` (string): Device UUID
- - `name` (string): Device name
- - `device_type` (string): Type (kobo, koreader, web)
- - `last_sync` (string): Last sync timestamp
- - `is_active` (boolean): Whether device is active
- - `created_at` (string): Registration timestamp
- - `sync_settings` (object): Device-specific sync settings
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Device not found
- - 500: Internal server error
-}
diff --git a/bruno/devices/Get Kobo Shelf.bru b/bruno/devices/Get Kobo Shelf.bru
deleted file mode 100644
index 129dc87..0000000
--- a/bruno/devices/Get Kobo Shelf.bru
+++ /dev/null
@@ -1,56 +0,0 @@
-meta {
- name: Get Kobo Shelf Books
- type: http
- seq: 1
-}
-
-get {
- url: {{baseURL}}/api/devices/{{deviceID}}/shelves?shelf={{shelfName}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{userToken}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Kobo Shelf Books
-
- Get all books on a Kobo device shelf, optionally filter by shelf name.
-
- **Method:** GET
-
- **Endpoint:** /api/devices/{deviceID}/shelves?shelf={shelfName}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `deviceID` (string): Device UUID
-
- **Query Parameters:**
- - `shelf` (string, optional): Shelf name to filter by. If omitted, returns all shelves.
-
- **Response:**
- - Array of books with:
- - `id` (string): Book UUID
- - `title` (string): Book title
- - `author` (string): Book author
- - `shelf_name` (string): Name of the shelf
- - `shelf_position` (number): Position on the shelf
- - `entitlement_id` (string): Kobo entitlement ID
- - `revision_id` (string): Kobo revision number
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Device not found
- - 500: Internal server error
-
- **Note:** Returns Kobo-specific metadata (entitlement ID, revision number) required for proper Kobo sync operations.
-}
diff --git a/bruno/devices/Initiate Device Registration.bru b/bruno/devices/Initiate Device Registration.bru
deleted file mode 100644
index 7857efc..0000000
--- a/bruno/devices/Initiate Device Registration.bru
+++ /dev/null
@@ -1,19 +0,0 @@
-meta {
- name: Initiate Device Registration
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/devices/register
- body: json
- auth: none
-}
-
-body:json {
- {
- "device_name": "My Kindle Paperwhite",
- "device_type": "koreader",
- "device_identifier": "kindle-pw5-hardware-id-12345"
- }
-}
diff --git a/bruno/devices/List Devices.bru b/bruno/devices/List Devices.bru
deleted file mode 100644
index 96e54cd..0000000
--- a/bruno/devices/List Devices.bru
+++ /dev/null
@@ -1,46 +0,0 @@
-meta {
- name: List Devices
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/devices
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## List Devices
-
- Lists all devices registered to the authenticated user's account.
-
- **Method:** GET
-
- **Endpoint:** /api/devices
-
- **Authentication:** Bearer token
-
- **Response:**
- - `devices` (array): Array of device objects
- - `id` (string): Device UUID
- - `name` (string): Device name
- - `device_type` (string): Type (kobo, koreader, web)
- - `last_sync` (string): Last sync timestamp
- - `is_active` (boolean): Whether device is active
- - `created_at` (string): Registration timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/devices/List Pending Registrations.bru b/bruno/devices/List Pending Registrations.bru
deleted file mode 100644
index ae63318..0000000
--- a/bruno/devices/List Pending Registrations.bru
+++ /dev/null
@@ -1,43 +0,0 @@
-meta {
- name: List Pending Device Registrations
- type: http
- seq: 5
-}
-
-get {
- url: {{base_url}}/api/devices/pending
- body: none
- auth: inherit
-}
-
-docs {
- ## List Pending Device Registrations
-
- Retrieves all pending device registration requests awaiting approval.
-
- **Method:** GET
-
- **Endpoint:** /api/devices/pending
-
- **Authentication:** Bearer token
-
- **Response:**
- - Array of pending device registrations
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
-
- **Example Response:**
- ```json
- [
- {
- "id": "uuid",
- "device_name": "My Kobo",
- "device_type": "kobo",
- "user_id": "uuid",
- "created_at": "2024-01-01T00:00:00Z"
- }
- ]
- ```
-}
diff --git a/bruno/devices/Reject Device Registration.bru b/bruno/devices/Reject Device Registration.bru
deleted file mode 100644
index ce983d7..0000000
--- a/bruno/devices/Reject Device Registration.bru
+++ /dev/null
@@ -1,42 +0,0 @@
-meta {
- name: Reject Device Registration
- type: http
- seq: 7
-}
-
-post {
- url: {{base_url}}/api/devices/reject/{{registration_id}}
- body: none
- auth: inherit
-}
-
-docs {
- ## Reject Device Registration
-
- Rejects a pending device registration request.
-
- **Method:** POST
-
- **Endpoint:** /api/devices/reject/:registration_id
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `registration_id` (string): Registration request UUID
-
- **Response:**
- - Success message confirming rejection
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Registration not found
- - 400: Invalid registration status
-
- **Example Response:**
- ```json
- {
- "message": "device registration rejected"
- }
- ```
-}
diff --git a/bruno/devices/Remove Book from Kobo Shelf.bru b/bruno/devices/Remove Book from Kobo Shelf.bru
deleted file mode 100644
index f3a6f16..0000000
--- a/bruno/devices/Remove Book from Kobo Shelf.bru
+++ /dev/null
@@ -1,49 +0,0 @@
-meta {
- name: Remove Book from Kobo Shelf
- type: http
- seq: 1
-}
-
-delete {
- url: {{baseURL}}/api/devices/{{deviceID}}/shelves?media_item_id={{bookUUID}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{userToken}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Remove Book from Kobo Shelf
-
- Remove a specific book from a Kobo device shelf, preventing it from syncing to that device.
-
- **Method:** DELETE
-
- **Endpoint:** /api/devices/{deviceID}/shelves?media_item_id={bookUUID}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `deviceID` (string): Device UUID
-
- **Query Parameters:**
- - `media_item_id` (string): Book UUID to remove from shelf
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success - book removed from shelf
- - 401: Unauthorized
- - 404: Device or book not found
- - 500: Internal server error
-
- **Note:** Removing a book from the device shelf prevents it from syncing to that device in future sync operations.
-}
diff --git a/bruno/devices/Update Device.bru b/bruno/devices/Update Device.bru
deleted file mode 100644
index e1abf96..0000000
--- a/bruno/devices/Update Device.bru
+++ /dev/null
@@ -1,20 +0,0 @@
-meta {
- name: Update Device
- type: http
- seq: 5
-}
-
-put {
- url: {{base_url}}/api/devices/{{deviceId}}
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "device_name": "My Updated Kindle",
- "sync_enabled": true,
- "auto_sync": true,
- "sync_frequency_minutes": 10
- }
-}
diff --git a/bruno/devices/api.bru b/bruno/devices/api.bru
deleted file mode 100644
index be94733..0000000
--- a/bruno/devices/api.bru
+++ /dev/null
@@ -1,134 +0,0 @@
-meta {
- name: "Bookhoard Device Management API"
- type: "collection"
- environment: {
- development: {
- base_url: "http://localhost:8765/api"
- },
- production: {
- base_url: "https://your-domain.com/api"
- }
- }
-}
-
-# Register Device
-@name("Register Device")
-POST {{environment.base_url}}/devices/register
-Content-Type: application/json
-{
- "device_name": "My Kobo Clara",
- "device_type": "kobo",
- "device_identifier": "N1234567890123"
-}
-
-@name("Register KOReader Device")
-POST {{environment.base_url}}/devices/register
-Content-Type: application/json
-{
- "device_name": "My Kindle Paperwhite",
- "device_type": "koreader",
- "device_identifier": "G090GP123456789"
-}
-
-@name("Register Web Device")
-POST {{environment.base_url}}/devices/register
-Content-Type: application/json
-{
- "device_name": "Chrome Browser",
- "device_type": "web",
- "device_identifier": "web-client-abc123"
-}
-
-# Check Registration Status
-@name("Check Pending Registration")
-POST {{environment.base_url}}/devices/register/status
-Content-Type: application/json
-{
- "registration_id": "registration-uuid-here"
-}
-
-@name("Check Approved Registration")
-POST {{environment.base_url}}/devices/register/status
-Content-Type: application/json
-{
- "registration_id": "registration-uuid-here"
-}
-
-# List Devices (requires authentication)
-@name("List User Devices")
-GET {{environment.base_url}}/devices
-Authorization: Bearer {{jwt_token}}
-
-@name("Get Device Details")
-GET {{environment.base_url}}/devices/{{device_id}}
-Authorization: Bearer {{jwt_token}}
-
-# Update Device Settings
-@name("Update Device Settings")
-PUT {{environment.base_url}}/devices/{{device_id}}
-Authorization: Bearer {{jwt_token}}
-Content-Type: application/json
-{
- "device_name": "Updated Device Name",
- "sync_enabled": true,
- "auto_sync": true,
- "sync_frequency_minutes": 10
-}
-
-@name("Disable Device Sync")
-PUT {{environment.base_url}}/devices/{{device_id}}
-Authorization: Bearer {{jwt_token}}
-Content-Type: application/json
-{
- "device_name": "My Kobo Clara",
- "sync_enabled": false,
- "auto_sync": false,
- "sync_frequency_minutes": 30
-}
-
-@name("Update Sync Frequency")
-PUT {{environment.base_url}}/devices/{{device_id}}
-Authorization: Bearer {{jwt_token}}
-Content-Type: application/json
-{
- "device_name": "My Kobo Clara",
- "sync_enabled": true,
- "auto_sync": true,
- "sync_frequency_minutes": 15
-}
-
-# Delete/Revoke Device
-@name("Delete Device")
-DELETE {{environment.base_url}}/devices/{{device_id}}
-Authorization: Bearer {{jwt_token}}
-
-# Get Pending Registrations
-@name("Get Pending Registrations")
-GET {{environment.base_url}}/devices/pending
-Authorization: Bearer {{jwt_token}}
-
-# Approve Device Registration
-@name("Approve Device Registration")
-GET {{environment.base_url}}/devices/approve/{{registration_id}}
-Authorization: Bearer {{jwt_token}}
-
-@name("Approve Registration - KOReader")
-GET {{environment.base_url}}/devices/approve/reg-uuid-123
-Authorization: Bearer {{jwt_token}}
-
-@name("Approve Registration - Kobo")
-GET {{environment.base_url}}/devices/approve/reg-uuid-456
-Authorization: Bearer {{jwt_token}}
-
-# Reject Device Registration
-@name("Reject Device Registration")
-POST {{environment.base_url}}/devices/reject/{{registration_id}}
-Authorization: Bearer {{jwt_token}}
-
-@name("Reject Registration - KOReader")
-POST {{environment.base_url}}/devices/reject/reg-uuid-123
-Authorization: Bearer {{jwt_token}}
-
-@name("Reject Registration - Kobo")
-POST {{environment.base_url}}/devices/reject/reg-uuid-456
-Authorization: Bearer {{jwt_token}}
diff --git a/bruno/devices/regenerate-token-forbidden.bru b/bruno/devices/regenerate-token-forbidden.bru
deleted file mode 100644
index cc33f00..0000000
--- a/bruno/devices/regenerate-token-forbidden.bru
+++ /dev/null
@@ -1,26 +0,0 @@
-meta {
- name: Regenerate Device Token - Forbidden
- type: http
- seq: 3
-}
-
-put {
- url: {{base_url}}/api/devices/{{other_device_id}}/regenerate-token
- body: none
- auth: inherit
-}
-
-docs {
- ## Regenerate Device Token - Forbidden
-
- Tests that users cannot regenerate tokens for devices belonging to other users.
-
- **Expected Behavior:** Returns 403 Forbidden when trying to regenerate token for another user's device
-
- **Status Codes:**
- - 403: Forbidden (device belongs to different user)
-
- **Use Case:** Verify authorization - users can only manage their own devices
-
- **Setup:** Use Bearer token from user A, try to regenerate token for user B's device
-}
diff --git a/bruno/devices/regenerate-token-notfound.bru b/bruno/devices/regenerate-token-notfound.bru
deleted file mode 100644
index 59d3a76..0000000
--- a/bruno/devices/regenerate-token-notfound.bru
+++ /dev/null
@@ -1,26 +0,0 @@
-meta {
- name: Regenerate Device Token - Not Found
- type: http
- seq: 4
-}
-
-put {
- url: {{base_url}}/api/devices/00000000-0000-0000-0000-000000000000/regenerate-token
- body: none
- auth: bearer
-}
-
-docs {
- ## Regenerate Device Token - Not Found
-
- Tests that token regeneration returns 404 for non-existent devices.
-
- **Expected Behavior:** Returns 404 Not Found when device UUID doesn't exist
-
- **Status Codes:**
- - 404: Device not found
-
- **Use Case:** Verify proper error handling for invalid device IDs
-
- **Setup:** Use all-zero UUID (guaranteed to not exist in database)
-}
diff --git a/bruno/devices/regenerate-token-unauthorized.bru b/bruno/devices/regenerate-token-unauthorized.bru
deleted file mode 100644
index 8680bd3..0000000
--- a/bruno/devices/regenerate-token-unauthorized.bru
+++ /dev/null
@@ -1,24 +0,0 @@
-meta {
- name: Regenerate Device Token - Unauthorized
- type: http
- seq: 2
-}
-
-put {
- url: {{base_url}}/api/devices/{{device_id}}/regenerate-token
- body: none
- auth: none
-}
-
-docs {
- ## Regenerate Device Token - Unauthorized
-
- Tests that token regeneration requires authentication.
-
- **Expected Behavior:** Returns 401 Unauthorized when no Bearer token is provided
-
- **Status Codes:**
- - 401: Unauthorized (missing or invalid token)
-
- **Use Case:** Verify authentication is required for token regeneration
-}
diff --git a/bruno/devices/regenerate-token.bru b/bruno/devices/regenerate-token.bru
deleted file mode 100644
index 9e79fe2..0000000
--- a/bruno/devices/regenerate-token.bru
+++ /dev/null
@@ -1,73 +0,0 @@
-meta {
- name: Regenerate Device Token
- type: http
- seq: 1
-}
-
-put {
- url: {{base_url}}/api/devices/{{device_id}}/regenerate-token
- body: none
- auth: inherit
-}
-
-docs {
- ## Regenerate Device Token
-
- Regenerates auth token for a device, invalidating old token immediately.
-
- **Method:** PUT
-
- **Endpoint:** /api/devices/{device_id}/regenerate-token
-
- **Authentication:** Bearer token (JWT)
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
-
- **Response:**
- - `message` (string): Success message
- - `auth_token` (string): New auth token
- - `device` (object): Updated device details
- - `sync_urls` (object): Device-specific sync URLs with new token
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (device belongs to different user)
- - 404: Device not found
- - 500: Internal server error
-
- **Important Notes:**
- - Old token stops working immediately
- - Device must be updated with new token to resume syncing
- - No data loss - device ID remains same
-
- **Example Response:**
- ```json
- {
- "message": "Token regenerated successfully",
- "auth_token": "dev_abc123...",
- "device": {
- "id": "uuid-here",
- "device_name": "My Kobo Clara",
- "device_type": "kobo",
- "sync_enabled": true,
- "auto_sync": true,
- "sync_frequency_minutes": 5,
- "created_at": "2026-02-12T10:00:00Z",
- "device_metadata": "{...}"
- },
- "sync_urls": {
- "sync_url": "http://localhost:8765/api/sync/kobo/dev_new_token",
- "markup": "http://localhost:8765/api/sync/kobo/dev_new_token/markup",
- "bookmark": "http://localhost:8765/api/sync/kobo/dev_new_token/bookmark",
- "init": "http://localhost:8765/api/sync/kobo/dev_new_token/v1/initialization"
- }
- }
- ```
-
- **Important Notes:**
- - Old token stops working immediately
- - Device must be updated with new token to resume syncing
- - No data loss - device ID remains same
-}
diff --git a/bruno/environments/Bookhoard.bru b/bruno/environments/Bookhoard.bru
deleted file mode 100644
index 68f6718..0000000
--- a/bruno/environments/Bookhoard.bru
+++ /dev/null
@@ -1,20 +0,0 @@
-vars {
- base_url: http://localhost:8765
- media_item_id: 02a535a4-19f8-43fa-b81b-89a226d19dd9
- fake_book_id: 123e4567-e89b-12d3-a456-426614174000
- user_id: c51118f0-31fc-4c32-827d-517d6599bf21
- highlight_id: 660f9501-f29b-51d4-b716-446655440001
- note_id: 7710a602-g29b-61d4-c716-446655440002
- library_id: cc23c3a7-f8fb-451a-a78d-2a16df1b725a
- job_id: 550e8400-e29b-41d4-a716-446655440000
- rating: 5
- is_visible: true
- library_folder: /app/uploads
- opds_base_url:
-}
-vars:secret [
- token,
- refresh_token,
- kobo_device_token,
- other_device_id
-]
diff --git a/bruno/highlights/Create Media Highlight.bru b/bruno/highlights/Create Media Highlight.bru
deleted file mode 100644
index 34abc50..0000000
--- a/bruno/highlights/Create Media Highlight.bru
+++ /dev/null
@@ -1,59 +0,0 @@
-meta {
- name: Create Media Highlight
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/media-items/{{media_item_id}}/highlights
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "selection_text": "This is the highlighted text from the media item.",
- "start_position": "page:45:offset:120",
- "end_position": "page:45:offset:145",
- "color": "#ffff00",
- "note_id": ""
- }
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Create Media Highlight
-
- Creates a new highlight for a specific media item.
-
- **Method:** POST
-
- **Endpoint:** /api/media-items/:id/highlights
-
- **Path Parameters:**
- - `id` (string): Media item ID
-
- **Request Body:**
- - `selection_text` (string): Highlighted text (required, 1-5000 chars)
- - `start_position` (string): Start position (required, max 100 chars)
- - `end_position` (string): End position (required, max 100 chars)
- - `color` (string): Highlight color in hex format (optional, default #ffff00)
- - `note_id` (string): Optional associated note ID
-
- **Response:**
- - Highlight object with all fields including generated ID and timestamps
-
- **Status Codes:**
- - 201: Created
- - 400: Invalid request
- - 401: Unauthorized
- - 404: Media item not found
-}
\ No newline at end of file
diff --git a/bruno/highlights/Delete Media Highlight.bru b/bruno/highlights/Delete Media Highlight.bru
deleted file mode 100644
index a8b6154..0000000
--- a/bruno/highlights/Delete Media Highlight.bru
+++ /dev/null
@@ -1,42 +0,0 @@
-meta {
- name: Delete Media Highlight
- type: http
- seq: 5
-}
-
-delete {
- url: {{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}
- body: none
- auth: inherit
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Media Highlight
-
- Deletes a specific highlight.
-
- **Method:** DELETE
-
- **Endpoint:** /api/media-items/:id/highlights/:highlightId
-
- **Path Parameters:**
- - `id` (string): Media item ID
- - `highlightId` (string): Highlight ID
-
- **Response:**
- - 204 No Content on success
-
- **Status Codes:**
- - 204: Success
- - 401: Unauthorized
- - 404: Highlight not found
-}
\ No newline at end of file
diff --git a/bruno/highlights/Get Media Highlights.bru b/bruno/highlights/Get Media Highlights.bru
deleted file mode 100644
index fa75cf1..0000000
--- a/bruno/highlights/Get Media Highlights.bru
+++ /dev/null
@@ -1,51 +0,0 @@
-meta {
- name: Get Media Highlights
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/media-items/{{media_item_id}}/highlights
- body: none
- auth: inherit
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Media Highlights
-
- Retrieves all highlights for a specific media item for the authenticated user.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/:id/highlights
-
- **Path Parameters:**
- - `id` (string): Media item ID
-
- **Response:**
- - Array of highlight objects with fields:
- - `id` (string): Highlight ID
- - `media_item_id` (string): Media item ID
- - `user_id` (string): User ID
- - `selection_text` (string): Highlighted text
- - `start_position` (string): Start position
- - `end_position` (string): End position
- - `color` (string): Highlight color (hex)
- - `note_id` (string): Optional associated note ID
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Media item not found
-}
\ No newline at end of file
diff --git a/bruno/highlights/Get Single Media Highlight.bru b/bruno/highlights/Get Single Media Highlight.bru
deleted file mode 100644
index 4d3f178..0000000
--- a/bruno/highlights/Get Single Media Highlight.bru
+++ /dev/null
@@ -1,42 +0,0 @@
-meta {
- name: Get Single Media Highlight
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}
- body: none
- auth: inherit
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Single Media Highlight
-
- Retrieves a specific highlight by ID.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/:id/highlights/:highlightId
-
- **Path Parameters:**
- - `id` (string): Media item ID
- - `highlightId` (string): Highlight ID
-
- **Response:**
- - Highlight object with all fields
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Highlight not found
-}
\ No newline at end of file
diff --git a/bruno/highlights/Update Media Highlight.bru b/bruno/highlights/Update Media Highlight.bru
deleted file mode 100644
index ae68582..0000000
--- a/bruno/highlights/Update Media Highlight.bru
+++ /dev/null
@@ -1,60 +0,0 @@
-meta {
- name: Update Media Highlight
- type: http
- seq: 4
-}
-
-put {
- url: {{base_url}}/api/media-items/{{media_item_id}}/highlights/{{highlight_id}}
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "selection_text": "This is the updated highlighted text.",
- "start_position": "page:45:offset:125",
- "end_position": "page:45:offset:150",
- "color": "#ffeb3b",
- "note_id": ""
- }
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Media Highlight
-
- Updates an existing highlight.
-
- **Method:** PUT
-
- **Endpoint:** /api/media-items/:id/highlights/:highlightId
-
- **Path Parameters:**
- - `id` (string): Media item ID
- - `highlightId` (string): Highlight ID
-
- **Request Body:**
- - `selection_text` (string): Updated highlighted text (required, 1-5000 chars)
- - `start_position` (string): Updated start position (required, max 100 chars)
- - `end_position` (string): Updated end position (required, max 100 chars)
- - `color` (string): Updated highlight color in hex format (optional)
- - `note_id` (string): Updated associated note ID (optional)
-
- **Response:**
- - Updated highlight object with all fields
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request
- - 401: Unauthorized
- - 404: Highlight not found
-}
\ No newline at end of file
diff --git a/bruno/kobo/Server Sync to Kobo.bru b/bruno/kobo/Server Sync to Kobo.bru
deleted file mode 100644
index 177cddf..0000000
--- a/bruno/kobo/Server Sync to Kobo.bru
+++ /dev/null
@@ -1,90 +0,0 @@
-meta {
- name: Sync from Bookhoard to Kobo
- type: http
- seq: 1
-}
-
-post {
- url: {{baseURL}}/api/sync/kobo/sync-from-server
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{koboToken}}
- Content-Type: application/json
- x-kobo-device: {"DeviceId":"kobo-clara-test","Model":"Kobo Clara","SerialNumber":"N123456789"}
-}
-
-body:json {
- [
- {
- "ContentId": "{{bookUUID}}",
- "PercentRead": 65.4,
- "LastModified": "2026-01-31T12:00:00Z",
- "Bookmarks": [
- {
- "BookmarkId": "bookmark-123",
- "ContentId": "{{bookUUID}}",
- "BookmarkText": "This is an important note",
- "BookmarkType": "bookmark",
- "BookmarkTitle": "Chapter 5 Note"
- }
- ],
- "Highlights": [
- {
- "BookmarkId": "highlight-456",
- "ContentId": "{{bookUUID}}",
- "BookmarkText": "highlighted passage text",
- "BookmarkType": "annotation",
- "BookmarkTitle": "Chapter 3 Highlight"
- }
- ]
- }
- ]
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Sync from Bookhoard to Kobo
-
- Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device. Two-way sync endpoint.
-
- **Method:** POST
-
- **Endpoint:** /api/sync/kobo/sync-from-server
-
- **Authentication:** Bearer token with Kobo device identification
-
- **Headers:**
- - `x-kobo-device` (string): JSON string containing Kobo device info
- - `DeviceId`: Kobo device ID
- - `Model`: Kobo device model
- - `SerialNumber`: Kobo device serial number
-
- **Request Body:** Array of sync data objects
- - `ContentId` (string): Book UUID
- - `PercentRead` (number): Reading progress percentage (0-100)
- - `LastModified` (string): ISO 8601 timestamp
- - `Bookmarks` (array, optional): Array of bookmark objects
- - `BookmarkId`: Unique bookmark ID
- - `ContentId`: Book UUID
- - `BookmarkText`: Bookmark text/note
- - `BookmarkType`: Type (bookmark, annotation, etc.)
- - `BookmarkTitle`: Bookmark title
- - `Highlights` (array, optional): Array of highlight objects (same structure as bookmarks)
-
- **Response:**
- - Sync result confirmation
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-
- **Note:** Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights.
-}
diff --git a/bruno/kobo/bookmark-sync.bru b/bruno/kobo/bookmark-sync.bru
deleted file mode 100644
index e48cf19..0000000
--- a/bruno/kobo/bookmark-sync.bru
+++ /dev/null
@@ -1,75 +0,0 @@
-meta {
- name: Kobo Bookmark Sync
- type: http
- seq: 3
-}
-
-post {
- url: {{base_url}}/api/v1/kobo/bookmark
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{device_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "BookmarkSync": [
- {
- "BookmarkId": "bookmark_2",
- "ContentId": "kobo_xyz789",
- "BookmarkText": "Important note",
- "BookmarkType": "bookmark",
- "DateCreated": "2026-01-31T12:00:00Z"
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests('Status is Success', body.Status === "Success");
- tests('BookmarksSynced >= 0', body.BookmarksSynced >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Kobo Bookmark Sync
-
- Synchronizes bookmarks from a Kobo device to the Bookhoard server.
-
- **Method:** POST
-
- **Endpoint:** /api/v1/kobo/bookmark
-
- **Authentication:** Bearer token (device token)
-
- **Request Body:**
- - `BookmarkSync` (array): Array of bookmark objects
- - `BookmarkId` (string): Unique bookmark ID
- - `ContentId` (string): Book/content ID
- - `BookmarkText` (string): Bookmark text or note
- - `BookmarkType` (string): Type (bookmark, highlight, note)
- - `DateCreated` (string): ISO 8601 timestamp
-
- **Response:**
- - `Status` (string): Sync status (Success, Partial)
- - `BookmarksSynced` (number): Number of bookmarks synced
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/kobo/initialization.bru b/bruno/kobo/initialization.bru
deleted file mode 100644
index f68c7f9..0000000
--- a/bruno/kobo/initialization.bru
+++ /dev/null
@@ -1,55 +0,0 @@
-meta {
- name: Kobo Initialization
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/v1/kobo/initialization
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{device_token}}
- Content-Type: application/json
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests('Has ContentId', body.ContentId !== undefined);
- tests('Has Categories', body.Categories !== undefined);
- tests('Has BookhoardUUID', body.BookhoardUUID !== undefined);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Kobo Initialization
-
- Initializes Kobo device sync, returning device resources and account information.
-
- **Method:** GET
-
- **Endpoint:** /api/v1/kobo/initialization
-
- **Authentication:** Bearer token (device token)
-
- **Response:**
- - `ContentId` (string): Device content ID
- - `Categories` (array): Available categories/collections
- - `BookhoardUUID` (string): Bookhoard instance UUID
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/kobo/markup-sync.bru b/bruno/kobo/markup-sync.bru
deleted file mode 100644
index 34ebc2f..0000000
--- a/bruno/kobo/markup-sync.bru
+++ /dev/null
@@ -1,95 +0,0 @@
-meta {
- name: Kobo Markup Sync
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/v1/kobo/markup
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{device_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "ReadingSync": [
- {
- "ContentId": "kobo_abc123def456",
- "PercentRead": 60.0,
- "RemainingTimeMin": 120,
- "ReadingEvent": "BookRead",
- "LastModified": "2026-01-31T12:00:00Z"
- }
- ],
- "BookmarkSync": [
- {
- "BookmarkId": "bookmark_1",
- "ContentId": "kobo_abc123def456",
- "BookmarkText": "Great quote",
- "BookmarkType": "annotation",
- "DateCreated": "2026-01-31T12:00:00Z"
- }
- ],
- "Metadata": true
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- const validStatus = body.Status === "Success" || body.Status === "Partial";
- tests('Status is Success or Partial', validStatus);
- tests('MarkupsSynced >= 0', body.MarkupsSynced >= 0);
- tests('BookmarksSynced >= 0', body.BookmarksSynced >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Kobo Markup Sync
-
- Synchronizes reading progress and markup (highlights, bookmarks) from a Kobo device.
-
- **Method:** POST
-
- **Endpoint:** /api/v1/kobo/markup
-
- **Authentication:** Bearer token (device token)
-
- **Request Body:**
- - `ReadingSync` (array, optional): Reading progress data
- - `ContentId` (string): Book/content ID
- - `PercentRead` (number): Percentage read (0-100)
- - `RemainingTimeMin` (number): Remaining time in minutes
- - `ReadingEvent` (string): Event type (BookRead, etc.)
- - `LastModified` (string): ISO 8601 timestamp
- - `BookmarkSync` (array, optional): Bookmark/highlight data
- - `BookmarkId` (string): Unique bookmark ID
- - `ContentId` (string): Book/content ID
- - `BookmarkText` (string): Highlighted/bookmarked text
- - `BookmarkType` (string): Type (annotation, bookmark)
- - `DateCreated` (string): ISO 8601 timestamp
- - `Metadata` (boolean): Whether to include metadata
-
- **Response:**
- - `Status` (string): Sync status (Success, Partial)
- - `MarkupsSynced` (number): Number of markups synced
- - `BookmarksSynced` (number): Number of bookmarks synced
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/koreader/Get Book Metadata.bru b/bruno/koreader/Get Book Metadata.bru
deleted file mode 100644
index 5c0e16d..0000000
--- a/bruno/koreader/Get Book Metadata.bru
+++ /dev/null
@@ -1,61 +0,0 @@
-meta {
- name: KOReader Get Book Metadata
- type: http
- seq: 2
-}
-
-get {
- url: {{base_url}}/api/sync/koreader/metadata/{{book_uuid}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{device_token}}
- Content-Type: application/json
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests['Status is 200'] = true;
- tests['Has UUID'] = body.uuid !== null;
- tests['Has title'] = body.title !== null;
- tests['Has progress'] = body.progress !== null;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## KOReader Get Book Metadata
-
- Retrieves metadata for a specific book from the KOReader sync endpoint.
-
- **Method:** GET
-
- **Endpoint:** /api/sync/koreader/metadata/{book_uuid}
-
- **Authentication:** Bearer token (device token)
-
- **Path Parameters:**
- - `book_uuid` (string): Book UUID
-
- **Response:**
- - `uuid` (string): Book UUID
- - `title` (string): Book title
- - `progress` (object): Reading progress data
- - `metadata` (object): Additional book metadata
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Book not found
- - 500: Internal server error
-}
diff --git a/bruno/koreader/Get Library.bru b/bruno/koreader/Get Library.bru
deleted file mode 100644
index 0a831c5..0000000
--- a/bruno/koreader/Get Library.bru
+++ /dev/null
@@ -1,55 +0,0 @@
-meta {
- name: KOReader Get Library
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/sync/koreader/library
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{device_token}}
- Content-Type: application/json
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests['Status is 200'] = res.getStatus() === 200;
- tests['Has library_sync'] = body.library_sync != null;
- tests('Total books >= 0', body.total_books >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## KOReader Get Library
-
- Retrieves the user's library for KOReader sync operations.
-
- **Method:** GET
-
- **Endpoint:** /api/sync/koreader/library
-
- **Authentication:** Bearer token (device token)
-
- **Response:**
- - `library_sync` (object): Library sync data
- - `total_books` (number): Total number of books
- - `books` (array): Array of book objects
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/koreader/Sync Annotations (Per-Book SHA-256).bru b/bruno/koreader/Sync Annotations (Per-Book SHA-256).bru
deleted file mode 100644
index 2238301..0000000
--- a/bruno/koreader/Sync Annotations (Per-Book SHA-256).bru
+++ /dev/null
@@ -1,86 +0,0 @@
-meta {
- name: KOReader Sync Annotations - Per-Book SHA-256
- type: http
- seq: 4
-}
-
-post {
- url: {{base_url}}/api/v1/koreader/sync/bookmarks
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{koreader_device_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "book_uuid": "{{book_uuid}}",
- "highlights": [
- {
- "text": "Quote from book 1",
- "pos0": "/6/4[chap1ref]!/4/2/1:0",
- "pos1": "/6/4[chap1ref]!/4/2/1:50",
- "color": "#ffff00",
- "page": 10,
- "book_sha256": "{{book_sha256}}"
- },
- {
- "text": "Quote from book 2 (different book)",
- "pos0": "/6/4[chap1ref]!/4/2/1:0",
- "pos1": "/6/4[chap1ref]!/4/2/1:50",
- "color": "#00ff00",
- "page": 15,
- "book_sha256": "{{another_book_sha256}}"
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests('Highlights synced >= 0', body.highlights_synced >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## KOReader Sync Annotations - Per-Book SHA-256
-
- Synchronizes annotations (highlights) from KOReader with per-annotation SHA-256 hashes for multi-book sync.
-
- **Method:** POST
-
- **Endpoint:** /api/v1/koreader/sync/bookmarks
-
- **Authentication:** Bearer token (KOReader device token)
-
- **Request Body:**
- - `book_uuid` (string): Primary book UUID
- - `highlights` (array): Array of highlight objects
- - `text` (string): Highlighted text
- - `pos0`, `pos1` (string): EPUB CFI positions
- - `color` (string): Highlight color (hex)
- - `page` (number): Page number
- - `book_sha256` (string): SHA-256 hash for this specific book
-
- **Response:**
- - `highlights_synced` (number): Number of highlights synced
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-
- **Note:** Each highlight can include its own book_sha256, allowing annotations from multiple books in a single request.
-}
diff --git a/bruno/koreader/Sync Bookmarks (SHA-256).bru b/bruno/koreader/Sync Bookmarks (SHA-256).bru
deleted file mode 100644
index 174c34c..0000000
--- a/bruno/koreader/Sync Bookmarks (SHA-256).bru
+++ /dev/null
@@ -1,90 +0,0 @@
-meta {
- name: KOReader Sync Bookmarks - SHA-256
- type: http
- seq: 3
-}
-
-post {
- url: {{base_url}}/api/v1/koreader/sync/bookmarks
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{koreader_device_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "book_sha256": "{{book_sha256}}",
- "bookmarks": [
- {
- "text": "Important passage about chapter 3",
- "pos0": "/6/4[chap3ref]!/4/2/1:0",
- "page": 45,
- "type": "bookmark"
- }
- ],
- "notes": [
- {
- "notes": "My note about this section",
- "pos0": "/6/4[chap3ref]!/4/2/1:100",
- "page": 47,
- "text": "Quoted text from the book"
- }
- ],
- "highlights": [
- {
- "text": "This is highlighted text",
- "pos0": "/6/4[chap3ref]!/4/2/1:50",
- "pos1": "/6/4[chap3ref]!/4/2/1:100",
- "color": "#ffff00",
- "page": 50
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests('Sync completed', body.sync_status === "completed");
- tests('Total synced >= 0', body.total_synced >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## KOReader Sync Bookmarks - SHA-256
-
- Synchronizes bookmarks, notes, and highlights from KOReader using SHA-256 hash for book identification.
-
- **Method:** POST
-
- **Endpoint:** /api/v1/koreader/sync/bookmarks
-
- **Authentication:** Bearer token (KOReader device token)
-
- **Request Body:**
- - `book_sha256` (string): SHA-256 hash of book file
- - `bookmarks` (array): Array of bookmarks
- - `notes` (array): Array of notes
- - `highlights` (array): Array of highlights
-
- **Response:**
- - `sync_status` (string): Sync status
- - `total_synced` (number): Total items synced
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/koreader/Sync Bookmarks.bru b/bruno/koreader/Sync Bookmarks.bru
deleted file mode 100644
index a7e388e..0000000
--- a/bruno/koreader/Sync Bookmarks.bru
+++ /dev/null
@@ -1,112 +0,0 @@
-meta {
- name: KOReader Sync Bookmarks
- type: http
- seq: 4
-}
-
-post {
- url: {{base_url}}/api/sync/koreader/bookmarks
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{device_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "book_uuid": "{{book_uuid}}",
- "bookmarks": [
- {
- "chapter": 3,
- "datetime": "2026-01-30T19:55:00Z",
- "notes": "Bookmarked text",
- "pos0": "epubcfi(/6/4/2:15)",
- "pos1": "epubcfi(/6/4/2:20)",
- "page": 45,
- "text": "This is important",
- "type": "highlight",
- "percentage": 0.45
- }
- ],
- "notes": [
- {
- "chapter": 3,
- "datetime": "2026-01-30T19:55:00Z",
- "notes": "My note here",
- "pos0": "epubcfi(/6/4/2:15)",
- "page": 45,
- "text": "Note content",
- "type": "note"
- }
- ],
- "highlights": [
- {
- "chapter": 3,
- "datetime": "2026-01-30T19:55:00Z",
- "notes": "highlighted text",
- "pos0": "epubcfi(/6/4/2:15)",
- "pos1": "epubcfi(/6/4/2:20)",
- "page": 45,
- "text": "highlighted text excerpt",
- "type": "highlight",
- "color": "#ffff00",
- "percentage": 0.45
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests['Sync completed'] = body.sync_status === "completed";
- tests('Items synced >= 0', body.total_synced >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## KOReader Sync Bookmarks
-
- Synchronizes bookmarks, notes, and highlights from a KOReader device.
-
- **Method:** POST
-
- **Endpoint:** /api/sync/koreader/bookmarks
-
- **Authentication:** Bearer token (device token)
-
- **Request Body:**
- - `book_uuid` (string): Book UUID
- - `bookmarks` (array): Array of bookmark objects
- - `notes` (array): Array of note objects
- - `highlights` (array): Array of highlight objects
-
- Each object includes:
- - `chapter` (number): Chapter number
- - `datetime` (string): ISO 8601 timestamp
- - `text` (string): Highlighted/bookmarked text
- - `pos0`, `pos1` (string): EPUB CFI positions
- - `page` (number): Page number
- - `type` (string): Type (highlight, bookmark, note)
- - `percentage` (number): Position in book (0-1)
-
- **Response:**
- - `sync_status` (string): Sync status (completed, partial)
- - `total_synced` (number): Number of items synced
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/koreader/Sync Progress (SHA-256 Only).bru b/bruno/koreader/Sync Progress (SHA-256 Only).bru
deleted file mode 100644
index 0e36f7f..0000000
--- a/bruno/koreader/Sync Progress (SHA-256 Only).bru
+++ /dev/null
@@ -1,76 +0,0 @@
-meta {
- name: KOReader Sync Progress - SHA-256 Only
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/v1/koreader/sync/progress
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{koreader_device_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "sync_mode": "immediate",
- "books": [
- {
- "sha256": "{{book_sha256}}",
- "file_path": "/mnt/onboard/Unknown%20Book.epub",
- "percentage": 0.45,
- "page": 89,
- "total_pages": 200
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- tests('Status is 200 or 202', res.getStatus() === 200 || res.getStatus() === 202);
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## KOReader Sync Progress - SHA-256 Only
-
- Synchronizes reading progress using only SHA-256 hash for book identification (when UUID is not available).
-
- **Method:** POST
-
- **Endpoint:** /api/v1/koreader/sync/progress
-
- **Authentication:** Bearer token (KOReader device token)
-
- **Request Body:**
- - `sync_mode` (string): Sync mode (immediate, deferred)
- - `books` (array): Array of book progress objects
- - `sha256` (string): SHA-256 hash of book file
- - `file_path` (string): Path to book file
- - `percentage` (number): Progress percentage
- - `page` (number): Current page
- - `total_pages` (number): Total pages
-
- **Response:**
- - `sync_status` (string): Sync status
- - `books_synced` (number): Number of books synced
-
- **Status Codes:**
- - 200: Success
- - 202: Accepted
- - 401: Unauthorized
- - 500: Internal server error
-
- **Note:** Use this when book UUID is not available, falling back to SHA-256 hash for identification.
-}
diff --git a/bruno/koreader/Sync Progress (SHA-256).bru b/bruno/koreader/Sync Progress (SHA-256).bru
deleted file mode 100644
index d847d31..0000000
--- a/bruno/koreader/Sync Progress (SHA-256).bru
+++ /dev/null
@@ -1,91 +0,0 @@
-meta {
- name: KOReader Sync Progress - SHA-256
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/v1/koreader/sync/progress
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{koreader_device_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "sync_mode": "immediate",
- "books": [
- {
- "uuid": "{{book_uuid}}",
- "sha256": "{{book_sha256}}",
- "file_path": "/mnt/onboard/The%20Hobbit.epub",
- "percentage": 0.65,
- "chapter": 5,
- "page": 142,
- "total_pages": 310,
- "epubcfi": "/6/4[chap1ref]!/4/2/1:0",
- "last_read": "2026-01-31T12:00:00Z",
- "title": "The Hobbit",
- "authors": ["J.R.R. Tolkien"]
- }
- ]
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200 || res.getStatus() === 202) {
- const body = res.getBody();
- tests['Status accepted'] = res.getStatus() === 200 || res.getStatus() === 202;
- tests('Has sync_status', body.sync_status !== undefined);
- tests('Books synced >= 0', body.books_synced >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## KOReader Sync Progress - SHA-256
-
- Synchronizes reading progress from a KOReader device using SHA-256 book hash for identification.
-
- **Method:** POST
-
- **Endpoint:** /api/v1/koreader/sync/progress
-
- **Authentication:** Bearer token (KOReader device token)
-
- **Request Body:**
- - `sync_mode` (string): Sync mode (immediate, deferred)
- - `books` (array): Array of book progress objects
- - `uuid` (string): Book UUID
- - `sha256` (string): SHA-256 hash of book file for identification
- - `file_path` (string): Path to book file on device
- - `percentage` (number): Progress percentage (0-1)
- - `chapter` (number): Current chapter
- - `page` (number): Current page
- - `total_pages` (number): Total pages
- - `epubcfi` (string): EPUB location
- - `last_read` (string): ISO 8601 timestamp
- - `title` (string): Book title
- - `authors` (array): List of authors
-
- **Response:**
- - `sync_status` (string): Sync status
- - `books_synced` (number): Number of books synced
-
- **Status Codes:**
- - 200: Success
- - 202: Accepted - processing
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/koreader/Sync Progress.bru b/bruno/koreader/Sync Progress.bru
deleted file mode 100644
index af11b6b..0000000
--- a/bruno/koreader/Sync Progress.bru
+++ /dev/null
@@ -1,98 +0,0 @@
-meta {
- name: KOReader Sync Progress
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/sync/koreader/progress
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{device_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "library_id": null,
- "books": [
- {
- "uuid": "{{book_uuid}}",
- "title": "Test Book",
- "authors": ["Test Author"],
- "progress": 0.45,
- "percentage": 0.45,
- "last_read": "2026-01-30T20:00:00Z",
- "chapter": 5,
- "character": 15432,
- "epubcfi": "epubcfi(/6/4/2:15)",
- "page": 89,
- "total_pages": 200
- }
- ],
- "sync_mode": "immediate",
- "device_info": {
- "koreader_version": "2024.01",
- "device_model": "kindle-paperwhite-5"
- }
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 202) {
- const body = res.getBody();
- tests['Status is 202'] = res.getStatus() === 202;
- tests['Sync status accepted'] = body.sync_status === "accepted";
- tests('Books synced >= 0', body.books_synced >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## KOReader Sync Progress
-
- Synchronizes reading progress from a KOReader device to the Bookhoard server.
-
- **Method:** POST
-
- **Endpoint:** /api/sync/koreader/progress
-
- **Authentication:** Bearer token (device token)
-
- **Request Body:**
- - `library_id` (string, optional): Library UUID
- - `books` (array): Array of book progress objects
- - `uuid` (string): Book UUID
- - `title` (string): Book title
- - `authors` (array): List of authors
- - `progress` (number): Progress value
- - `percentage` (number): Percentage complete (0-1)
- - `last_read` (string): ISO 8601 timestamp
- - `chapter` (number): Current chapter
- - `epubcfi` (string): EPUB Canonical Fragment Identifier
- - `page` (number): Current page
- - `total_pages` (number): Total pages
- - `sync_mode` (string): Sync mode (immediate, deferred)
- - `device_info` (object): Device information
- - `koreader_version` (string): KOReader version
- - `device_model` (string): Device model identifier
-
- **Response:**
- - `sync_status` (string): Sync status (accepted, processing)
- - `books_synced` (number): Number of books synced
-
- **Status Codes:**
- - 202: Accepted - sync queued
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/library/Add Library Folder.bru b/bruno/library/Add Library Folder.bru
deleted file mode 100644
index 1482211..0000000
--- a/bruno/library/Add Library Folder.bru
+++ /dev/null
@@ -1,65 +0,0 @@
-meta {
- name: Add Library Folder
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/libraries/{{library_id}}/folders
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "folder_path": {{library_folder}}
- }
-}
-
-vars:pre-request {
- libraryId: "8821b703-h29b-71d4-d716-446655440003"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Add Library Folder
-
- Adds a new folder path to a library for media scanning and indexing.
-
- **Method:** POST
-
- **Endpoint:** /api/libraries/{id}/folders
-
- **Authentication:** Required (Bearer token, admin permissions)
-
- **Path Parameters:**
- - `id` (string, required): Library UUID
-
- **Request Body:**
- - `folder_path` (string, required): Absolute path to the folder containing media files
-
- **Response:** Folder object
- - `id` (string): Folder UUID
- - `library_id` (string): Library UUID
- - `folder_path` (string): Absolute path to folder
- - `is_active` (boolean): Whether folder is active for scanning
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 201: Folder added successfully
- - 400: Invalid folder path or request data
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Library not found
- - 409: Folder already exists for this library
- - 500: Internal server error
-}
diff --git a/bruno/library/Create Library.bru b/bruno/library/Create Library.bru
deleted file mode 100644
index 5be9824..0000000
--- a/bruno/library/Create Library.bru
+++ /dev/null
@@ -1,80 +0,0 @@
-meta {
- name: Create Library
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/libraries
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "name": "My Ebook Library",
- "description": "A collection of technical books and novels",
- "type": "ebooks"
- }
-}
-
-script:post-response {
- function onResponse(res) {
- let data = res.getBody();
- // If successful registration, set token environment variable
- if (res.getStatus() === 201 || res.getStatus() === 200) {
- if (data && data.id) {
-
- return bru.setEnvVar("library_id", data.id, { persist: true });
- }
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Create Library
-
- Creates a new library for organizing media items.
-
- **Method:** POST
-
- **Endpoint:** /api/libraries
-
- **Authentication:** Required (Bearer token)
-
- **Request Body:**
- - `name` (string, required): Library name
- - `description` (string, optional): Library description
- - `type` (string, required): Library type
- - `"ebooks"`: Electronic books
- - `"audiobooks"`: Audio books
- - `"videos"`: Video content
- - `"other"`: Other media types
-
- **Response:** Library object
- - `id` (string): Library UUID
- - `name` (string): Library name
- - `description` (string, optional): Library description
- - `type` (string): Library type
- - `is_visible` (boolean): Library visibility status
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 201: Library created successfully
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden (insufficient permissions)
- - 409: Library name already exists
- - 500: Internal server error
-}
diff --git a/bruno/library/Delete Library Folder.bru b/bruno/library/Delete Library Folder.bru
deleted file mode 100644
index 7dbbe76..0000000
--- a/bruno/library/Delete Library Folder.bru
+++ /dev/null
@@ -1,75 +0,0 @@
-meta {
- name: Delete Library Folder
- type: http
- seq: 4
-}
-
-delete {
- url: {{base_url}}/api/libraries/{{library_id}}/folders
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- "folder_path": "/path/to/folder"
-}
-
-tests {
- test_delete_library_folder_success(status, headers, body) {
- if (status !== 204) {
- throw new Error("Expected status 204, got " + status);
- }
-
- // Delete should return no content
- if (body && body.length > 0) {
- throw new Error("Expected empty response body for delete");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Library Folder
-
- Removes a folder from a library's scanning configuration.
-
- **Method:** DELETE
-
- **Endpoint:** /api/libraries/{id}/folders
- **Authentication:** Required (Bearer token, admin only)
-
- **Path Parameters:**
- - `id` (string): Library UUID
-
- **Request Body:**
- - `folder_path` (string, required): Path to folder to remove
-
- **Response:** Empty (204 No Content)
-
- **Status Codes:**
- - 204: Folder deleted successfully
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Library not found
- - 500: Internal server error
-
- **Examples:**
- - Delete folder: `DELETE /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a/folders`
-
- **Note:** Admin access required - only users with admin role can manage library folders.
-}
\ No newline at end of file
diff --git a/bruno/library/Delete Library.bru b/bruno/library/Delete Library.bru
deleted file mode 100644
index 451d200..0000000
--- a/bruno/library/Delete Library.bru
+++ /dev/null
@@ -1,71 +0,0 @@
-meta {
- name: Delete Library
- type: http
- seq: 3
-}
-
-delete {
- url: {{base_url}}/api/libraries/{{library_id}}
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_delete_library_success(status, headers, body) {
- if (status !== 204) {
- throw new Error("Expected status 204, got " + status);
- }
-
- // Delete should return no content
- if (body && body.length > 0) {
- throw new Error("Expected empty response body for delete");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Library
-
- Deletes a library and all associated media items.
-
- **Method:** DELETE
-
- **Endpoint:** /api/libraries/{id}
-
- **Authentication:** Required (Bearer token, admin only)
-
- **Path Parameters:**
- - `id` (string): Library UUID
-
- **Response:** Empty (204 No Content)
-
- **Status Codes:**
- - 204: Library deleted successfully
- - 400: Invalid library ID
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Library not found
- - 500: Internal server error
-
- **Warning:** This will delete ALL media items in the library.
-
- **Examples:**
- - Delete library: `DELETE /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a`
-
- **Note:** Admin access required - only users with admin role can delete libraries.
-}
\ No newline at end of file
diff --git a/bruno/library/Get Libraries (Admin).bru b/bruno/library/Get Libraries (Admin).bru
deleted file mode 100644
index 62127b7..0000000
--- a/bruno/library/Get Libraries (Admin).bru
+++ /dev/null
@@ -1,49 +0,0 @@
-meta {
- name: Get Libraries (Admin)
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/libraries
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Libraries (Admin)
-
- Retrieves all libraries in the system (admin access required).
-
- **Method:** GET
-
- **Endpoint:** /api/libraries
-
- **Authentication:** Required (Bearer token, admin permissions)
-
- **Response:** Array of library objects
- - `id` (string): Library UUID
- - `name` (string): Library name
- - `description` (string, optional): Library description
- - `type` (string): Library type (ebooks, audiobooks, videos, other)
- - `is_visible` (boolean): Library visibility to users
- - `media_count` (number): Number of media items in library
- - `folder_count` (number): Number of folders associated
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 500: Internal server error
-}
diff --git a/bruno/library/Get Library Folders.bru b/bruno/library/Get Library Folders.bru
deleted file mode 100644
index 7ca5317..0000000
--- a/bruno/library/Get Library Folders.bru
+++ /dev/null
@@ -1,55 +0,0 @@
-meta {
- name: Get Library Folders
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/libraries/{{library_id}}/folders
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-vars:pre-request {
- libraryId: "8821b703-h29b-71d4-d716-446655440003"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Library Folders
-
- Retrieves all folders associated with a specific library.
-
- **Method:** GET
-
- **Endpoint:** /api/libraries/{id}/folders
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string, required): Library UUID
-
- **Response:** Array of folder objects
- - `id` (string): Folder UUID
- - `library_id` (string): Library UUID
- - `folder_path` (string): Absolute path to the folder
- - `is_active` (boolean): Whether the folder is currently active for scanning
- - `last_scanned` (string, optional): Timestamp of last scan
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (library access denied)
- - 404: Library not found
- - 500: Internal server error
-}
diff --git a/bruno/library/Get Library Stats.bru b/bruno/library/Get Library Stats.bru
deleted file mode 100644
index 5f95b0d..0000000
--- a/bruno/library/Get Library Stats.bru
+++ /dev/null
@@ -1,92 +0,0 @@
-meta {
- name: Get Library Stats
- type: http
- seq: 5
-}
-
-get {
- url: {{base_url}}/api/libraries/{{library_id}}/stats
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_get_library_stats_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- const contentType = headers["content-type"];
- if (!contentType || !contentType.includes("application/json")) {
- throw new Error("Expected content-type to contain application/json, got " + contentType);
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data || typeof data !== "object") {
- throw new Error("Expected stats object in response");
- }
-
- // Expected fields (verify they exist)
- const expectedFields = ["total_items", "total_size", "scanned_at"];
- for (const field of expectedFields) {
- if (!(field in data)) {
- console.warn("Stats field '" + field + "' is missing from response");
- }
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- libraryId: "cc23d3a7-f8fb-451a-a78d-2a16df1b725a"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Library Stats
-
- Retrieves statistical information about a library's media items.
-
- **Method:** GET
-
- **Endpoint:** /api/libraries/{id}/stats
-
- **Authentication:** Required (Bearer token, admin only)
-
- **Path Parameters:**
- - `id` (string): Library UUID
-
- **Response:** Stats object
- - `total_items` (number): Total media items in library
- - `total_size` (number): Total file size in bytes
- - `scanned_at` (string): Last scan timestamp
- - Additional fields may be included
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid library ID
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Library not found
- - 500: Internal server error
-
- **Examples:**
- - Get library stats: `GET /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a/stats`
-
- **Note:** Admin access required - only users with admin role can access library statistics.
-}
\ No newline at end of file
diff --git a/bruno/library/Get Library Types.bru b/bruno/library/Get Library Types.bru
deleted file mode 100644
index 7eb01af..0000000
--- a/bruno/library/Get Library Types.bru
+++ /dev/null
@@ -1,42 +0,0 @@
-meta {
- name: Get Library Types
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/libraries/types
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Library Types
-
- Retrieves all available library types that can be used when creating libraries.
-
- **Method:** GET
-
- **Endpoint:** /api/libraries/types
-
- **Authentication:** Required (Bearer token)
-
- **Response:** Array of library type objects
- - `id` (string): Type identifier (e.g., "ebooks", "audiobooks", "videos", "other")
- - `name` (string): Display name for the type (e.g., "Ebooks", "Audiobooks", "Videos", "Other")
- - `description` (string, optional): Description of what this type is used for
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/library/Get Library.bru b/bruno/library/Get Library.bru
deleted file mode 100644
index 3f9b623..0000000
--- a/bruno/library/Get Library.bru
+++ /dev/null
@@ -1,92 +0,0 @@
-meta {
- name: Get Library
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/libraries/{{library_id}}
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_get_library_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- const contentType = headers["content-type"];
- if (!contentType || !contentType.includes("application/json")) {
- throw new Error("Expected content-type to contain application/json, got " + contentType);
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- // Verify library object structure
- if (!data || typeof data !== "object") {
- throw new Error("Expected library object in response");
- }
-
- // Required fields
- if (!data.id || !data.name || !data.library_type_id) {
- throw new Error("Library missing required fields: id, name, library_type_id");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Library
-
- Retrieves detailed information about a specific library by ID.
-
- **Method:** GET
-
- **Endpoint:** /api/libraries/{id}
-
- **Authentication:** Required (Bearer token, admin only)
-
- **Path Parameters:**
- - `id` (string): Library UUID
-
- **Response:** Library object
- - `id` (string): Library UUID
- - `name` (string): Library name
- - `description` (string): Library description
- - `library_type_id` (string): Library type UUID
- - `created_by_admin_id` (string): Admin UUID who created it
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Library not found
- - 500: Internal server error
-
- **Examples:**
- - Get library: `GET /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a`
-
- **Note:** Admin access required - only users with admin role can access library details.
-}
\ No newline at end of file
diff --git a/bruno/library/Get Scan Settings.bru b/bruno/library/Get Scan Settings.bru
deleted file mode 100644
index 355d211..0000000
--- a/bruno/library/Get Scan Settings.bru
+++ /dev/null
@@ -1,45 +0,0 @@
-meta {
- name: Get Scan Settings
- type: http
- seq: 2
-}
-
-get {
- url: {{base_url}}/api/library/scan-settings
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Scan Settings
-
- Retrieves the user's current library scanning settings.
-
- **Method:** GET
-
- **Endpoint:** /api/library/scan-settings
-
- **Authentication:** Required (Bearer token)
-
- **Response:**
- - `scan_frequency_minutes` (number): Minutes between automatic scans (minimum 1)
- - `auto_scan_enabled` (boolean): Whether automatic scanning is enabled
- - `last_scan_at` (string, optional): Timestamp of last scan
- - `next_scan_at` (string, optional): Timestamp of next scheduled scan
- - `library_id` (string, optional): Library ID for context
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (access denied)
- - 500: Internal server error
-}
diff --git a/bruno/library/Get User Visible Libraries.bru b/bruno/library/Get User Visible Libraries.bru
deleted file mode 100644
index 72dcdbd..0000000
--- a/bruno/library/Get User Visible Libraries.bru
+++ /dev/null
@@ -1,46 +0,0 @@
-meta {
- name: Get User Visible Libraries
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/libraries/visible
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get User Visible Libraries
-
- Retrieves all libraries that are visible to regular users.
-
- **Method:** GET
-
- **Endpoint:** /api/libraries/visible
-
- **Authentication:** Required (Bearer token)
-
- **Response:** Array of visible library objects
- - `id` (string): Library UUID
- - `name` (string): Library name
- - `description` (string, optional): Library description
- - `type` (string): Library type (ebooks, audiobooks, videos, other)
- - `is_visible` (boolean): Always true for this endpoint
- - `media_count` (number): Number of media items in library
- - `created_at` (string): Creation timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/library/Set Library Visibility.bru b/bruno/library/Set Library Visibility.bru
deleted file mode 100644
index 9a8bac2..0000000
--- a/bruno/library/Set Library Visibility.bru
+++ /dev/null
@@ -1,63 +0,0 @@
-meta {
- name: Set Library Visibility
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/libraries/visibility
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "library_id": "{{library_id}}",
- "is_visible": {{is_visible}}
- }
-}
-
-vars:pre-request {
- libraryId: "8821b703-h29b-71d4-d716-446655440003",
- isVisible: true
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Set Library Visibility
-
- Updates the visibility status of a library for regular users.
-
- **Method:** POST
-
- **Endpoint:** /api/libraries/visibility
-
- **Authentication:** Required (Bearer token, admin permissions)
-
- **Request Body:**
- - `library_id` (string, required): Library UUID
- - `is_visible` (boolean, required): Visibility status
- - `true`: Library visible to all users
- - `false`: Library hidden from regular users
-
- **Response:** Updated library visibility object
- - `library_id` (string): Library UUID
- - `is_visible` (boolean): Updated visibility status
- - `updated_at` (string): Update timestamp
-
- **Status Codes:**
- - 200: Visibility updated successfully
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Library not found
- - 500: Internal server error
-}
diff --git a/bruno/library/Update Library.bru b/bruno/library/Update Library.bru
deleted file mode 100644
index f1fcb21..0000000
--- a/bruno/library/Update Library.bru
+++ /dev/null
@@ -1,93 +0,0 @@
-meta {
- name: Update Library
- type: http
- seq: 2
-}
-
-put {
- url: {{base_url}}/api/libraries/{{library_id}}
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- "name": "Updated Library Name",
- "description": "Updated library description"
-}
-
-tests {
- test_update_library_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data || typeof data !== "object") {
- throw new Error("Expected updated library object in response");
- }
-
- if (!data.id || !data.updated_at) {
- throw new Error("Library response missing update confirmation");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Library
-
- Updates an existing library's information.
-
- **Method:** PUT
-
- **Endpoint:** /api/libraries/{id}
-
- **Authentication:** Required (Bearer token, admin only)
-
- **Path Parameters:**
- - `id` (string): Library UUID
-
- **Request Body:**
- - `name` (string, optional): Library name
- - `description` (string, optional): Library description
-
- **Response:** Updated library object
- - `id` (string): Library UUID
- - `name` (string): Updated library name
- - `description` (string): Updated library description
- - `library_type_id` (string): Library type UUID
- - `updated_at` (string): Update timestamp
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Library not found
- - 500: Internal server error
-
- **Examples:**
- - Update library: `PUT /api/libraries/cc23c3a7-f8fb-451a-a78d-2a16df1b725a`
-
- **Note:** Admin access required - only users with admin role can update libraries.
-}
\ No newline at end of file
diff --git a/bruno/library/Update Scan Settings.bru b/bruno/library/Update Scan Settings.bru
deleted file mode 100644
index 608288f..0000000
--- a/bruno/library/Update Scan Settings.bru
+++ /dev/null
@@ -1,51 +0,0 @@
-meta {
- name: Update Scan Settings
- type: http
- seq: 1
-}
-
-put {
- url: {{base_url}}/api/library/scan-settings
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "scan_frequency_minutes": 60,
- "auto_scan_enabled": true
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Scan Settings
-
- Updates the user's media scanning settings.
-
- **Method:** PUT
-
- **Endpoint:** /api/library/scan-settings
-
- **Authentication:** Required
-
- **Request Body:**
- - `scan_frequency_minutes` (integer, required): Minutes between automatic scans (15-1440)
- - `auto_scan_enabled` (boolean, required): Whether automatic scanning is enabled
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid settings
- - 401: Unauthorized
-}
diff --git a/bruno/media-items/Create Media Item.bru b/bruno/media-items/Create Media Item.bru
deleted file mode 100644
index 67f2db4..0000000
--- a/bruno/media-items/Create Media Item.bru
+++ /dev/null
@@ -1,197 +0,0 @@
-meta {
- name: Create Media Item
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/media-items
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
- body:json {
- "library_id": "{{library_id}}",
- "title": "New Media Item",
- "author": "Author Name",
- "isbn": "978-0123456789",
- "description": "Description of the media item",
- "cover_image_path": "/path/to/cover.jpg",
- "series": "Series Name",
- "series_number": 1,
- "tags": ["science fiction", "ACME CORP.", "non-fiction"],
- "asin": "B08XYZ123",
- "date_published": "2023-01-15",
- "publisher": "Publisher Name",
- "contributors": ["O'Reilly Media", "acme corp"],
- "language": "en",
- "edition": "First Edition",
- "page_count": 350,
- "genre": "Science Fiction",
- "copyright_year": 2023,
- "goodreads_id": "123456",
- "openlibrary_id": "OL123456M",
- "google_books_id": "GB123456"
- }
-
-tests {
- test_create_media_item_success(status, headers, body) {
- if (status !== 201) {
- throw new Error("Expected status 201, got " + status);
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data || typeof data !== "object") {
- throw new Error("Expected media item object in response");
- }
-
- // Verify required fields
- if (!data.id || !data.title || !data.library_id) {
- throw new Error("Media item missing required fields: id, title, library_id");
- }
-
- return true;
- }
-
- test_create_media_item_no_folders(status, headers, body) {
- if (status !== 400) {
- throw new Error("Expected status 400 for library with no folders, got " + status);
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data || typeof data !== 'object') {
- throw new Error("Expected error object in response");
- }
-
- if (!data.error || typeof data.error !== 'string') {
- throw new Error("Expected error message in response");
- }
-
- if (!data.error.includes("folder")) {
- throw new Error("Error message should mention folders requirement");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- libraryId: "cc23c3a7-f8fb-451a-a78d-2a16df1b725a"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
- docs {
- ## Create Media Item
-
- Creates a new media item in a library with full metadata.
-
- **Method:** POST
-
- **Endpoint:** /api/media-items
-
- **Authentication:** Required (Bearer token, admin only)
-
- **Prerequisites**
- - Library must have at least one folder configured before media items can be added
- - Use `POST /api/libraries/{library_id}/folders` to add folders first
- - See: [Add Library Folder](../../library/Add%20Library%20Folder.bru)
-
- **Request Body:**
- - `library_id` (string, required): Library UUID
- - `title` (string, required): Media item title
- - `author` (string, optional): Author name
- - `isbn` (string, optional): ISBN number
- - `description` (string, optional): Description
- - `cover_image_path` (string, optional): Path to cover image
- - `series` (string, optional): Series name
- - `series_number` (integer, optional): Number in series
- - `tags` (array of string, optional): Tags or categories (auto-normalized) (automatically normalized)
- - `asin` (string, optional): Amazon ASIN
- - `date_published` (string, optional): Publication date
- - `publisher` (string, optional): Publisher name
- - `contributors` (array of string, optional): List of contributors (auto-normalized) (automatically normalized)
- - `language` (string, optional): Language code (ISO 639-1)
- - `edition` (string, optional): Edition information
- - `page_count` (integer, optional): Total page count
- - `genre` (string, optional): Genre classification
- - `copyright_year` (integer, optional): Copyright year
- - `goodreads_id` (string, optional): Goodreads identifier
- - `openlibrary_id` (string, optional): Open Library identifier
- - `google_books_id` (string, optional): Google Books identifier
-
- **Response:** Created media item object
- - All fields above plus:
- - `tags_search` (array): Normalized for search (lowercase, no punctuation)
- - `contributors_search` (array): Normalized for search (lowercase, no punctuation)
-
- **Normalization Behavior:**
-
- Tags are automatically normalized:
- - Trim whitespace
- - Titlecased (preserves hyphenation: "non-fiction" → "Non-Fiction")
- - Case-insensitive deduplication (keeps version with punctuation if exists)
- - Example: `["science fiction", "SCIENCE-FICTION"]` → `["Science-Fiction"]`
-
- Contributors are automatically normalized:
- - Trim whitespace
- - Preserve original casing (CAPSLOCK companies, Title Case, etc.)
- - Preserve punctuation for display
- - Case-insensitive deduplication (keeps version with punctuation if exists)
- - Example: `["acme corp", "ACME CORP.", "acme corp"]` → `["ACME CORP."]`
-
- **Validation Error Response**
- - **400 Bad Request** - Library has no folders:
- ```json
- {
- "error": "Cannot add media items to a library with no folders. Please add at least one folder to the library first."
- }
- ```
-
- **Status Codes:**
- - 201: Media item created successfully
- - 400: Invalid request data OR library has no folders
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Library not found
- - 500: Internal server error
-
- **Setup Workflow Example:**
- ```bash
- # 1. Create library
- POST /api/libraries
- { "name": "My Books", "type": "ebooks" }
-
- # 2. Add folder to library (REQUIRED before adding media items)
- POST /api/libraries/{library_id}/folders
- { "folder_path": "/mnt/books/my-library" }
-
- # 3. Create media items (now that library has folders)
- POST /api/media-items
- { "library_id": "...", "title": "Book Title", ... }
- ```
-
- **Examples:**
- - Create media item: `POST /api/media-items`
-
- **Note:** Admin access required - only users with admin role can create media items.
- }
\ No newline at end of file
diff --git a/bruno/media-items/Create Media Rating.bru b/bruno/media-items/Create Media Rating.bru
deleted file mode 100644
index 9639e2c..0000000
--- a/bruno/media-items/Create Media Rating.bru
+++ /dev/null
@@ -1,71 +0,0 @@
-meta {
- name: Create Media Rating
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/media-items/{{media_item_id}}/rating
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "rating": 8
- }
-}
-
-vars:pre-request {
- mediaItemId: "9932c704-i29b-81d4-e716-446655440004"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Create Media Rating
-
- Creates or updates the authenticated user's rating for a specific media item.
-
- **Method:** POST
-
- **Endpoint:** /api/media-items/{id}/rating
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string, required): Media item UUID
-
- **Request Body:**
- - `rating` (number, required): Rating value (1-10, where odd numbers = half-stars)
- - 1,3,5,7,9 = 0.5,1.5,2.5,3.5,4.5 stars (half-star precision)
- - 2,4,6,8,10 = 1,2,3,4,5 stars (full stars)
-
- **Example Request:**
- - `"rating": 7` = 3.5 stars (frontend display)
- - `"rating": 8` = 4.0 stars (frontend display)
-
- **Response:** Rating object
- - `id` (string): Rating UUID
- - `media_item_id` (string): Media item UUID
- - `user_id` (string): User UUID
- - `rating` (number): Rating value (1-10)
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 201: Rating created successfully
- - 200: Rating updated successfully (if rating already existed)
- - 400: Invalid rating value (must be 1-10)
- - 401: Unauthorized
- - 403: Forbidden (rating access denied)
- - 404: Media item not found
- - 500: Internal server error
-}
diff --git a/bruno/media-items/Delete Media Item.bru b/bruno/media-items/Delete Media Item.bru
deleted file mode 100644
index 5a0a809..0000000
--- a/bruno/media-items/Delete Media Item.bru
+++ /dev/null
@@ -1,71 +0,0 @@
-meta {
- name: Delete Media Item
- type: http
- seq: 3
-}
-
-delete {
- url: {{base_url}}/api/media-items/{{media_item_id}}
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_delete_media_item_success(status, headers, body) {
- if (status !== 204) {
- throw new Error("Expected status 204, got " + status);
- }
-
- // Delete should return no content
- if (body && body.length > 0) {
- throw new Error("Expected empty response body for delete");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- mediaItemId: "550e8400-e29b-41d4-a716-446655440000"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Media Item
-
- Deletes a media item from the library.
-
- **Method:** DELETE
-
- **Endpoint:** /api/media-items/{id}
-
- **Authentication:** Required (Bearer token, admin only)
-
- **Path Parameters:**
- - `id` (string): Media item UUID
-
- **Response:** Empty (204 No Content)
-
- **Status Codes:**
- - 204: Media item deleted successfully
- - 400: Invalid media item ID
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Media item not found
- - 500: Internal server error
-
- **Examples:**
- - Delete media item: `DELETE /api/media-items/550e8400-e29b-41d4-a716-446655440000`
-
- **Warning:** This permanently removes the media item and all associated data (ratings, notes, highlights).
-
- **Note:** Admin access required - only users with admin role can delete media items.
-}
\ No newline at end of file
diff --git a/bruno/media-items/Delete Media Rating.bru b/bruno/media-items/Delete Media Rating.bru
deleted file mode 100644
index 537af50..0000000
--- a/bruno/media-items/Delete Media Rating.bru
+++ /dev/null
@@ -1,67 +0,0 @@
-meta {
- name: Delete Media Rating
- type: http
- seq: 5
-}
-
-delete {
- url: {{base_url}}/api/media-items/{{media_item_id}}/rating
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_delete_media_rating_success(status, headers, body) {
- if (status !== 204) {
- throw new Error("Expected status 204, got " + status);
- }
-
- // Delete should return no content
- if (body && body.length > 0) {
- throw new Error("Expected empty response body for delete");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- mediaItemId: "550e8400-e29b-41d4-a716-446655440000"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Media Rating
-
- Deletes a user's rating for a specific media item.
-
- **Method:** DELETE
-
- **Endpoint:** /api/media-items/{id}/rating
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string): Media item UUID
-
- **Response:** Empty (204 No Content)
-
- **Status Codes:**
- - 204: Rating deleted successfully
- - 401: Unauthorized
- - 404: Media item not found
- - 500: Internal server error
-
- **Examples:**
- - Delete media rating: `DELETE /api/media-items/550e8400-e29b-41d4-a716-446655440000/rating`
-
- **Note:** Deletes only the authenticated user's rating, not other users' ratings.
-}
\ No newline at end of file
diff --git a/bruno/media-items/EPUB Download.bru b/bruno/media-items/EPUB Download.bru
deleted file mode 100644
index 2973d1d..0000000
--- a/bruno/media-items/EPUB Download.bru
+++ /dev/null
@@ -1,42 +0,0 @@
-meta {
- name: Download Media Item
- type: http
- seq: 1
-}
-
-get {
- url: {{baseURL}}/api/media-items/{{bookUUID}}/download
- body: none
- auth: none
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Download Media Item
-
- Download a media item file (EPUB, PDF, etc.) from Bookhoard server.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/{bookUUID}/download
-
- **Authentication:** None (for Kobo device downloads)
-
- **Path Parameters:**
- - `bookUUID` (string): Media item UUID
-
- **Response:** Binary file data (EPUB, PDF, etc.)
-
- **Response Headers:**
- - `Content-Type`: application/epub+zip, application/pdf, or appropriate MIME type
-
- **Status Codes:**
- - 200: Success - File returned
- - 404: Media item not found
-
- **Note:** This endpoint is designed for Kobo devices to download media items directly from Bookhoard. The endpoint returns the file with appropriate Content-Type headers.
-}
diff --git a/bruno/media-items/Filter Media Items.bru b/bruno/media-items/Filter Media Items.bru
deleted file mode 100644
index a14222b..0000000
--- a/bruno/media-items/Filter Media Items.bru
+++ /dev/null
@@ -1,73 +0,0 @@
-meta {
- name: Filter Media Items
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/media-items/filtered?library_id={{library_id}}&genre_filter=Fiction&language_filter=en&year_min=2000&year_max=2024&limit=10&offset=0
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-vars:pre-request {
- genre: "Fiction"
- language: "en"
- yearMin: 2000
- yearMax: 2024
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Filter Media Items
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/filtered
-
- **Authentication:** Required (Bearer token)
-
- **Query Parameters:**
- - `library_id` (string, required): UUID of the library
- - `author_filter` (string, optional): Filter by author (partial match)
- - `series_filter` (string, optional): Filter by series (partial match)
- - `genre_filter` (string, optional): Filter by genre (exact match)
- - `language_filter` (string, optional): Filter by language (exact match, e.g., 'en', 'es', 'fr')
- - `year_min` (integer, optional): Minimum copyright year
- - `year_max` (integer, optional): Maximum copyright year
- - `has_cover` (boolean, optional): Filter for items with cover images only
- - `sort` (string, optional): Sort field and direction (same options as ListMediaItems)
- - `limit` (integer, optional): Number of items to return (default: 50, max: 1000)
- - `offset` (integer, optional): Number of items to skip (default: 0)
-
- **Response:** Object containing array of filtered media items
-
- **Status Codes:**
- - 200: Success
- - 400: Bad request (invalid parameters)
- - 401: Unauthorized
- - 500: Internal server error
-
- **Examples:**
- - Filter by genre: `/api/media-items/filtered?library_id=xxx&genre_filter=Fiction`
- - Filter by language: `/api/media-items/filtered?library_id=xxx&language_filter=es`
- - Filter by year range: `/api/media-items/filtered?library_id=xxx&year_min=2000&year_max=2024`
- - Filter by cover: `/api/media-items/filtered?library_id=xxx&has_cover=true`
- - Combine filters: `/api/media-items/filtered?library_id=xxx&genre_filter=Sci-Fi&year_min=2010&language_filter=en`
-
- **Filter Behavior:**
- - Multiple filters can be combined (AND logic)
- - Author and series filters use partial matching (ILIKE)
- - Genre and language filters use exact matching
- - Year range filters are inclusive
- - Filters are applied before sorting and pagination
- - User library visibility is respected
-}
diff --git a/bruno/media-items/Get Media Item.bru b/bruno/media-items/Get Media Item.bru
deleted file mode 100644
index d71f054..0000000
--- a/bruno/media-items/Get Media Item.bru
+++ /dev/null
@@ -1,113 +0,0 @@
-meta {
- name: Get Media Item
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/media-items/{{media_item_id}}
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_get_media_item_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- const contentType = headers["content-type"];
- if (!contentType || !contentType.includes("application/json")) {
- throw new Error("Expected content-type to contain application/json, got " + contentType);
- }
-
- // Verify response body is valid JSON and has expected structure
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data || typeof data !== "object") {
- throw new Error("Expected response body to be an object");
- }
-
- // Check for required fields in media item
- if (!data.id) {
- throw new Error("Media item missing required field: id");
- }
-
- if (!data.title) {
- throw new Error("Media item missing required field: title");
- }
-
- if (!data.library_id) {
- throw new Error("Media item missing required field: library_id");
- }
-
- if (!data.media_type) {
- throw new Error("Media item missing required field: media_type");
- }
-
- // Validate data types
- if (typeof data.id !== "string") {
- throw new Error("Media item id must be a string");
- }
-
- if (typeof data.title !== "string") {
- throw new Error("Media item title must be a string");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- mediaItemId: "9932c704-i29b-81d4-e716-446655440004"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Media Item
-
- Retrieves detailed information about a specific media item.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/{id}
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string, required): Media item UUID
-
- **Response:** Media item object
- - `id` (string): Media item UUID
- - `title` (string): Media item title
- - `description` (string, optional): Media description
- - `library_id` (string): Library UUID
- - `media_type` (string): Type of media (e.g., "ebook", "audiobook")
- - `file_path` (string): Path to media file
- - `file_size` (number, optional): File size in bytes
- - `metadata` (object, optional): Additional media metadata
- - `author` (string, optional): Author name (for books)
- - `isbn` (string, optional): ISBN number
- - `duration` (number, optional): Duration in seconds (for audiobooks)
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (access denied)
- - 404: Media item not found
- - 500: Internal server error
-}
\ No newline at end of file
diff --git a/bruno/media-items/Get Media Rating.bru b/bruno/media-items/Get Media Rating.bru
deleted file mode 100644
index 2cbe9c9..0000000
--- a/bruno/media-items/Get Media Rating.bru
+++ /dev/null
@@ -1,84 +0,0 @@
-meta {
- name: Get Media Rating
- type: http
- seq: 4
-}
-
-get {
- url: {{base_url}}/api/media-items/{{media_item_id}}/rating
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_get_media_rating_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data || typeof data !== "object") {
- throw new Error("Expected rating object in response");
- }
-
- // Verify expected fields
- if (!data.media_item_id || !data.rating === undefined) {
- throw new Error("Rating response missing required fields: media_item_id, rating");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- mediaItemId: "550e8400-e29b-41d4-a716-446655440000"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Media Rating
-
- Retrieves a user's rating for a specific media item.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/{id}/rating
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string): Media item UUID
-
- **Response:** Rating object
- - `id` (string): Rating UUID
- - `media_item_id` (string): Media item UUID
- - `user_id` (string): User UUID
- - `rating` (integer): Rating value (1-10 scale)
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Media item not found
- - 500: Internal server error
-
- **Examples:**
- - Get media rating: `GET /api/media-items/550e8400-e29b-41d4-a716-446655440000/rating`
-
- **Note:** Returns the authenticated user's rating for the specified media item.
-}
\ No newline at end of file
diff --git a/bruno/media-items/List Media Items Sorted.bru b/bruno/media-items/List Media Items Sorted.bru
deleted file mode 100644
index 83b34be..0000000
--- a/bruno/media-items/List Media Items Sorted.bru
+++ /dev/null
@@ -1,75 +0,0 @@
-meta {
- name: List Media Items with Sorting
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/media-items?library_id={{library_id}}&sort=title+ASC&limit=10&offset=0
- body: none
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-vars:pre-request {
- sortBy: "title ASC"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## List Media Items with Sorting
-
- **Method:** GET
-
- **Endpoint:** /api/media-items
-
- **Authentication:** Required (Bearer token)
-
- **Query Parameters:**
- - `library_id` (string, required): UUID of the library
- - `sort` (string, optional): Sort field and direction
- - Available options:
- - `created_at ASC` - Oldest added first
- - `created_at DESC` - Newest added first (default)
- - `title ASC` - Title A-Z
- - `title DESC` - Title Z-A
- - `author ASC` - Author A-Z
- - `author DESC` - Author Z-A
- - `series ASC` - Series order
- - `series DESC` - Series reverse order
- - `date_published ASC` - Oldest published first
- - `date_published DESC` - Newest published first
- - `copyright_year ASC` - Oldest copyright first
- - `copyright_year DESC` - Newest copyright first
- - `page_count ASC` - Shortest first
- - `page_count DESC` - Longest first
- - `genre ASC` - Genre A-Z
- - `genre DESC` - Genre Z-A
- - `limit` (integer, optional): Number of items to return (default: 50, max: 1000)
- - `offset` (integer, optional): Number of items to skip (default: 0)
-
- **Response:** Object containing array of media items
-
- **Status Codes:**
- - 200: Success
- - 400: Bad request (invalid parameters)
- - 401: Unauthorized
- - 500: Internal server error
-
- **Examples:**
- - Sort by title: `/api/media-items?library_id=xxx&sort=title+ASC`
- - Sort by author descending: `/api/media-items?library_id=xxx&sort=author+DESC`
- - Sort by page count: `/api/media-items?library_id=xxx&sort=page_count+ASC`
-
- **Sorting Behavior:**
- - All sorts are secondary-sorted by series_number then title for consistency
- - NULL values are sorted last for ascending, first for descending
- - Sorting is case-insensitive for text fields
-}
diff --git a/bruno/media-items/List Media Items.bru b/bruno/media-items/List Media Items.bru
deleted file mode 100644
index 93ec6e0..0000000
--- a/bruno/media-items/List Media Items.bru
+++ /dev/null
@@ -1,103 +0,0 @@
-meta {
- name: List Media Items
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/media-items?library_id={{library_id}}&limit=20&offset=0
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_list_media_items_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- const contentType = headers["content-type"];
- if (!contentType || !contentType.includes("application/json")) {
- throw new Error("Expected content-type to contain application/json, got " + contentType);
- }
-
- // Verify response body is valid JSON and has expected structure
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!Array.isArray(data)) {
- throw new Error("Expected response body to be an array");
- }
-
- // Validate each media item in the array
- data.forEach((item, index) => {
- if (!item || typeof item !== "object") {
- throw new Error("Media item at index " + index + " is not an object");
- }
-
- if (!item.id) {
- throw new Error("Media item at index " + index + " missing required field: id");
- }
-
- if (!item.title) {
- throw new Error("Media item at index " + index + " missing required field: title");
- }
-
- if (!item.library_id) {
- throw new Error("Media item at index " + index + " missing required field: library_id");
- }
- });
-
- return true;
- }
-}
-
-vars:pre-request {
- libraryId: "8821b703-h29b-71d4-d716-446655440003"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## List Media Items
-
- Retrieves a paginated list of media items from a specific library.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items
-
- **Authentication:** Required (Bearer token)
-
- **Query Parameters:**
- - `library_id` (string, required): Library UUID to filter items
- - `limit` (number, optional): Number of results per page (default: 20, max: 100)
- - `offset` (number, optional): Pagination offset (default: 0)
-
- **Response:** Array of media item objects
- - `id` (string): Media item UUID
- - `title` (string): Media item title
- - `library_id` (string): Library UUID
- - `media_type` (string): Type of media (e.g., "ebook", "audiobook")
- - `file_path` (string): Path to media file
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid query parameters
- - 401: Unauthorized
- - 403: Forbidden (library access denied)
- - 404: Library not found
- - 500: Internal server error
-}
\ No newline at end of file
diff --git a/bruno/media-items/Search Media Items.bru b/bruno/media-items/Search Media Items.bru
deleted file mode 100644
index d981527..0000000
--- a/bruno/media-items/Search Media Items.bru
+++ /dev/null
@@ -1,120 +0,0 @@
-meta {
- name: Search Media Items
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/media-items/search?q=harry
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_search_media_items_success(status, headers, body) {
- if (status !== 200 && status !== 404) {
- throw new Error("Expected status 200 or 404, got " + status);
- }
-
- const contentType = headers["content-type"];
- if (!contentType || !contentType.includes("application/json")) {
- throw new Error("Expected content-type to contain application/json, got " + contentType);
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (status === 404) {
- if (!data.error || data.error !== "no results found") {
- throw new Error("Expected error message 'no results found' for 404 status");
- }
- return true;
- }
-
- if (!Array.isArray(data)) {
- throw new Error("Expected response body to be an array");
- }
-
- data.forEach((item, index) => {
- if (!item || typeof item !== "object") {
- throw new Error("Media item at index " + index + " is not an object");
- }
-
- if (!item.id) {
- throw new Error("Media item at index " + index + " missing required field: id");
- }
-
- if (!item.title) {
- throw new Error("Media item at index " + index + " missing required field: title");
- }
-
- if (!item.library_id) {
- throw new Error("Media item at index " + index + " missing required field: library_id");
- }
- });
-
- return true;
- }
-}
-
-vars:pre-request {
- searchQuery: "harry"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Search Media Items
-
- Performs a search across all visible media items using partial matching with fuzzy fallback.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/search
-
- **Authentication:** Required (Bearer token)
-
- **Query Parameters:**
- - `q` (string, required): Search query (minimum 2 characters)
-
- **Search Behavior:**
- 1. First performs case-insensitive partial matching across:
- - Title
- - Author
- - Series
- - Tags
- - Contributors
- 2. If no results found, falls back to fuzzy search using word_similarity with 0.3 threshold
-
- **Response:** Array of media item objects (same structure as List Media Items)
-
- **Status Codes:**
- - 200: Success (results found)
- - 404: No results found
- - 400: Missing or invalid query parameter
- - 401: Unauthorized
- - 500: Internal server error
-
- **Examples:**
- - Search by title: `q=harry potter`
- - Search by author: `q=king`
- - Fuzzy search: `q=hary poter` (will find "harry potter")
-
- **Ranking:**
- Results are ranked by relevance:
- - Title matches: Highest priority
- - Author matches: High priority
- - Series matches: Medium priority
- - Tag matches: Lower priority
- - Fuzzy matches: Sorted by similarity score
-}
\ No newline at end of file
diff --git a/bruno/media-items/Update Media Item.bru b/bruno/media-items/Update Media Item.bru
deleted file mode 100644
index 3ef6fa8..0000000
--- a/bruno/media-items/Update Media Item.bru
+++ /dev/null
@@ -1,107 +0,0 @@
-meta {
- name: Update Media Item
- type: http
- seq: 2
-}
-
-put {
- url: {{base_url}}/api/media-items/{{media_item_id}}
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- "title": "Updated Media Item Title",
- "author": "Updated Author Name",
- "isbn": "978-9876543210",
- "description": "Updated description",
- "cover_image_path": "/updated/path/to/cover.jpg",
- "series": "Updated Series Name",
- "series_number": 2,
- "tags": ["updated", "fiction", "adventure"],
- "asin": "B09XYZ789",
- "date_published": "2023-02-20",
- "publisher": "Updated Publisher",
- "contributors": ["Updated Contributor"],
- "language": "en",
- "edition": "Updated Edition",
- "page_count": 400,
- "genre": "Updated Genre",
- "copyright_year": 2023,
- "goodreads_id": "7890123",
- "openlibrary_id": "OL789012M",
- "google_books_id": "GB789012"
-}
-
-tests {
- test_update_media_item_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data || typeof data !== "object") {
- throw new Error("Expected updated media item object in response");
- }
-
- if (!data.id || !data.updated_at) {
- throw new Error("Media item response missing update confirmation");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- mediaItemId: "550e8400-e29b-41d4-a716-446655440000"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Media Item
-
- Updates an existing media item's metadata.
-
- **Method:** PUT
-
- **Endpoint:** /api/media-items/{id}
-
- **Authentication:** Required (Bearer token, admin only)
-
- **Path Parameters:**
- - `id` (string): Media item UUID
-
- **Request Body:** All media item fields (same as Create)
- - Tags and contributors are auto-normalized (see Create Media Item docs)
-
- **Response:** Updated media item object
- - Includes normalized `tags` and `contributors` fields
- - Includes updated `tags_search` and `contributors_search` fields
-
- **Status Codes:**
- - 200: Media item updated successfully
- - 400: Invalid request data
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Media item not found
- - 500: Internal server error
-
- **Examples:**
- - Update media item: `PUT /api/media-items/550e8400-e29b-41d4-a716-446655440000`
-
- **Note:** Admin access required - only users with admin role can update media items.
-}
\ No newline at end of file
diff --git a/bruno/media-items/Update Media Rating.bru b/bruno/media-items/Update Media Rating.bru
deleted file mode 100644
index a017676..0000000
--- a/bruno/media-items/Update Media Rating.bru
+++ /dev/null
@@ -1,95 +0,0 @@
-meta {
- name: Update Media Rating
- type: http
- seq: 1
-}
-
-put {
- url: {{base_url}}/api/media-items/{{media_item_id}}/rating
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "rating": 4,
- "review": "Great book! Very enjoyable read."
- }
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_update_media_rating_success(status, headers, body) {
- if (status !== 200) {
- throw new Error("Expected status 200, got " + status);
- }
-
- const contentType = headers["content-type"];
- if (!contentType || !contentType.includes("application/json")) {
- throw new Error("Expected content-type to contain application/json");
- }
-
- let data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- throw new Error("Response body is not valid JSON");
- }
-
- if (!data.id) {
- throw new Error("Response missing id field");
- }
-
- if (typeof data.rating !== "number") {
- throw new Error("Rating should be a number");
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- mediaItemId: "8821b703-1234-5678-9123-446655440001"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Media Rating
-
- Updates an existing rating for a media item.
-
- **Method:** PUT
-
- **Endpoint:** /api/media-items/:id/rating
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string, required): Media item UUID
-
- **Request Body:**
- - `rating` (number, required): Rating value (typically 1-5)
- - `review` (string, optional): Review text
-
- **Response:** Updated rating object
- - `id` (string): Rating ID
- - `media_item_id` (string): Media item UUID
- - `rating` (number): Rating value
- - `review` (string): Review text
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request body
- - 401: Unauthorized
- - 404: Media item or rating not found
- - 500: Internal server error
-}
diff --git a/bruno/notes/Create Media Note.bru b/bruno/notes/Create Media Note.bru
deleted file mode 100644
index a8261d1..0000000
--- a/bruno/notes/Create Media Note.bru
+++ /dev/null
@@ -1,53 +0,0 @@
-meta {
- name: Create Media Note
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/media-items/{{media_item_id}}/notes
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "content": "This is a test note about this media item.",
- "position": "page:45"
- }
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Create Media Note
-
- Creates a new note for a specific media item.
-
- **Method:** POST
-
- **Endpoint:** /api/media-items/:id/notes
-
- **Path Parameters:**
- - `id` (string): Media item ID
-
- **Request Body:**
- - `content` (string): Note content (required, 1-10000 chars)
- - `position` (string): Optional position reference (max 100 chars)
-
- **Response:**
- - Note object with all fields including generated ID and timestamps
-
- **Status Codes:**
- - 201: Created
- - 400: Invalid request
- - 401: Unauthorized
- - 404: Media item not found
-}
\ No newline at end of file
diff --git a/bruno/notes/Delete Media Note.bru b/bruno/notes/Delete Media Note.bru
deleted file mode 100644
index 0bdf83e..0000000
--- a/bruno/notes/Delete Media Note.bru
+++ /dev/null
@@ -1,42 +0,0 @@
-meta {
- name: Delete Media Note
- type: http
- seq: 5
-}
-
-delete {
- url: {{base_url}}/api/media-items/{{media_item_id}}/notes/{{note_id}}
- body: none
- auth: inherit
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Media Note
-
- Deletes a specific note.
-
- **Method:** DELETE
-
- **Endpoint:** /api/media-items/:id/notes/:noteId
-
- **Path Parameters:**
- - `id` (string): Media item ID
- - `noteId` (string): Note ID
-
- **Response:**
- - 204 No Content on success
-
- **Status Codes:**
- - 204: Success
- - 401: Unauthorized
- - 404: Note not found
-}
\ No newline at end of file
diff --git a/bruno/notes/Get Media Notes.bru b/bruno/notes/Get Media Notes.bru
deleted file mode 100644
index ac84348..0000000
--- a/bruno/notes/Get Media Notes.bru
+++ /dev/null
@@ -1,48 +0,0 @@
-meta {
- name: Get Media Notes
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/media-items/{{media_item_id}}/notes
- body: none
- auth: inherit
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Media Notes
-
- Retrieves all notes for a specific media item for the authenticated user.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/:id/notes
-
- **Path Parameters:**
- - `id` (string): Media item ID
-
- **Response:**
- - Array of note objects with fields:
- - `id` (string): Note ID
- - `media_item_id` (string): Media item ID
- - `user_id` (string): User ID
- - `content` (string): Note content
- - `position` (string): Optional position reference
- - `created_at` (string): Creation timestamp
- - `updated_at` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Media item not found
-}
\ No newline at end of file
diff --git a/bruno/notes/Get Single Media Note.bru b/bruno/notes/Get Single Media Note.bru
deleted file mode 100644
index a6b7bd7..0000000
--- a/bruno/notes/Get Single Media Note.bru
+++ /dev/null
@@ -1,42 +0,0 @@
-meta {
- name: Get Single Media Note
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/media-items/{{media_item_id}}/notes/{{note_id}}
- body: none
- auth: inherit
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Single Media Note
-
- Retrieves a specific note by ID.
-
- **Method:** GET
-
- **Endpoint:** /api/media-items/:id/notes/:noteId
-
- **Path Parameters:**
- - `id` (string): Media item ID
- - `noteId` (string): Note ID
-
- **Response:**
- - Note object with all fields
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Note not found
-}
\ No newline at end of file
diff --git a/bruno/notes/Update Media Note.bru b/bruno/notes/Update Media Note.bru
deleted file mode 100644
index a3a15b5..0000000
--- a/bruno/notes/Update Media Note.bru
+++ /dev/null
@@ -1,54 +0,0 @@
-meta {
- name: Update Media Note
- type: http
- seq: 4
-}
-
-put {
- url: {{base_url}}/api/media-items/{{media_item_id}}/notes/{{note_id}}
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "content": "This is the updated note content.",
- "position": "page:47"
- }
-}
-
-script:post-response {
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Media Note
-
- Updates an existing note.
-
- **Method:** PUT
-
- **Endpoint:** /api/media-items/:id/notes/:noteId
-
- **Path Parameters:**
- - `id` (string): Media item ID
- - `noteId` (string): Note ID
-
- **Request Body:**
- - `content` (string): Updated note content (required, 1-10000 chars)
- - `position` (string): Updated position reference (optional, max 100 chars)
-
- **Response:**
- - Updated note object with all fields
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request
- - 401: Unauthorized
- - 404: Note not found
-}
\ No newline at end of file
diff --git a/bruno/opds/Download Book - Query Token.bru b/bruno/opds/Download Book - Query Token.bru
deleted file mode 100644
index f09c0ba..0000000
--- a/bruno/opds/Download Book - Query Token.bru
+++ /dev/null
@@ -1,39 +0,0 @@
-meta {
- name: Download Book - Query Token
- type: http
- seq: 3
-}
-
-get {
- url: {{opds_base_url}}/opds/devices/{{device_id}}/download/{{book_id}}?token={{device_token}}
-}
-
-docs {
- ## Download Book - Query Token
-
- Tests OPDS book download using query parameter authentication.
-
- **Method:** GET
-
- **Endpoint:** /opds/devices/{device_id}/download/{book_id}?token={device_token}
-
- **Authentication:** Query parameter (for Kobo devices)
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
- - `book_id` (string): Book UUID
- - `token` (string): Device auth_token
-
- **Response:** Book file (EPUB)
-
- **Status Codes:**
- - 200: Success (book file)
- - 401: Unauthorized (missing or invalid token)
- - 403: Device sync disabled
- - 404: Device not found
- - 500: Book not found
-
- **Important:** Kobo devices use query parameter for OPDS downloads
-
- **Use Case:** Kobo devices downloading books from OPDS
-}
diff --git a/bruno/opds/Download Book EPUB.bru b/bruno/opds/Download Book EPUB.bru
deleted file mode 100644
index dbc02a8..0000000
--- a/bruno/opds/Download Book EPUB.bru
+++ /dev/null
@@ -1,11 +0,0 @@
-meta {
- name: Download Book (EPUB)
- type: http
- seq: 3
-}
-
-get {
- url: {{opds_base_url}}/devices/{{device_id}}/download/{{book_id}}
-}
-
-vars:device_id, book_id, opds_base_url
diff --git a/bruno/opds/Download Book KEPUB (On-the-fly Conversion).bru b/bruno/opds/Download Book KEPUB (On-the-fly Conversion).bru
deleted file mode 100644
index 93b4479..0000000
--- a/bruno/opds/Download Book KEPUB (On-the-fly Conversion).bru
+++ /dev/null
@@ -1,71 +0,0 @@
-meta {
- name: Download Book KEPUB (On-the-fly Conversion)
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/opds/devices/{{deviceId}}/download/{{mediaItemId}}?format=kepub
- body: none
- auth: inherit
-}
-
-script:post-response {
- function onResponse(res) {
- const headers = res.getHeaders();
-
- const kepubHash = headers.get('X-Bookhoard-KEPUB-SHA256');
- if (kepubHash) {
- tests['KEPUB hash present'] = true;
- tests['Hash is 64 chars'] = kepubHash.length === 64;
- } else {
- tests['KEPUB hash present'] = false;
- }
-
- const bookhoardUUID = headers.get('X-Bookhoard-UUID');
- tests['Bookhoard UUID present'] = bookhoardUUID !== null;
-
- const contentType = headers.get('Content-Type');
- tests['Content-Type is KEPUB'] = contentType && contentType.includes('kepub');
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Download Book KEPUB (On-the-fly Conversion)
-
- Downloads a book in Kobo EPUB (KEPUB) format with on-the-fly conversion if needed.
-
- **Method:** GET
-
- **Endpoint:** /opds/devices/{deviceId}/download/{mediaItemId}?format=kepub
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `deviceId` (string): Device UUID
- - `mediaItemId` (string): Media item UUID
-
- **Query Parameters:**
- - `format` (string): Must be "kepub"
-
- **Response Headers:**
- - `X-Bookhoard-KEPUB-SHA256` (string): SHA-256 hash of the KEPUB file (64 chars)
- - `X-Bookhoard-UUID` (string): Bookhoard UUID for the media item
- - `Content-Type` (string): Will be "application/kepub+json" or similar
-
- **Response Body:** Binary KEPUB file data
-
- **Status Codes:**
- - 200: Success - KEPUB file returned
- - 401: Unauthorized
- - 404: Media item or device not found
- - 500: Internal server error or conversion failure
-
- **Note:** The KEPUB format adds special Kobo-specific markup to enhance reading features on Kobo devices. The file is converted on-the-fly if the source is not already KEPUB.
-}
diff --git a/bruno/opds/Download Book KEPUB.bru b/bruno/opds/Download Book KEPUB.bru
deleted file mode 100644
index fbb0365..0000000
--- a/bruno/opds/Download Book KEPUB.bru
+++ /dev/null
@@ -1,11 +0,0 @@
-meta {
- name: Download Book (KEPUB)
- type: http
- seq: 4
-}
-
-get {
- url: {{opds_base_url}}/devices/{{device_id}}/download/{{book_id}}?format=kepub
-}
-
-vars:device_id, book_id, opds_base_url
diff --git a/bruno/opds/Get Cover Image.bru b/bruno/opds/Get Cover Image.bru
deleted file mode 100644
index 3de3ad6..0000000
--- a/bruno/opds/Get Cover Image.bru
+++ /dev/null
@@ -1,11 +0,0 @@
-meta {
- name: Get Cover Image
- type: http
- seq: 5
-}
-
-get {
- url: {{opds_base_url}}/devices/{{device_id}}/cover/{{book_id}}
-}
-
-vars:device_id, book_id, opds_base_url
diff --git a/bruno/opds/Get Device Catalog - Bearer.bru b/bruno/opds/Get Device Catalog - Bearer.bru
deleted file mode 100644
index a01b27c..0000000
--- a/bruno/opds/Get Device Catalog - Bearer.bru
+++ /dev/null
@@ -1,36 +0,0 @@
-meta {
- name: Get Device Catalog - Bearer
- type: http
- seq: 1
-}
-
-get {
- url: {{opds_base_url}}/opds/devices/{{device_id}}/catalog
-}
-
-docs {
- ## Get Device OPDS Catalog - Bearer
-
- Tests OPDS device catalog retrieval using Bearer token authentication.
-
- **Method:** GET
-
- **Endpoint:** /opds/devices/{device_id}/catalog
-
- **Authentication:** Bearer token (for KOReader, API clients, other devices)
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
-
- **Response:** OPDS Atom feed catalog
-
- **Status Codes:**
- - 200: Success (OPDS catalog)
- - 401: Unauthorized (missing or invalid token)
- - 403: Device sync disabled
- - 404: Device not found
-
- **Important:** Bearer token in Authorization header
-
- **Use Case:** KOReader devices, API clients, or any device configured with Bearer token
-}
diff --git a/bruno/opds/Get Device Catalog - Query Token.bru b/bruno/opds/Get Device Catalog - Query Token.bru
deleted file mode 100644
index f971554..0000000
--- a/bruno/opds/Get Device Catalog - Query Token.bru
+++ /dev/null
@@ -1,37 +0,0 @@
-meta {
- name: Get Device Catalog - Query Token
- type: http
- seq: 2
-}
-
-get {
- url: {{opds_base_url}}/opds/devices/{{device_id}}/catalog?token={{device_token}}
-}
-
-docs {
- ## Get Device OPDS Catalog - Query Token
-
- Tests OPDS device catalog retrieval using query parameter authentication.
-
- **Method:** GET
-
- **Endpoint:** /opds/devices/{device_id}/catalog?token={device_token}
-
- **Authentication:** Query parameter (for Kobo devices)
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
- - `token` (string): Device auth_token
-
- **Response:** OPDS Atom feed catalog
-
- **Status Codes:**
- - 200: Success (OPDS catalog)
- - 401: Unauthorized (missing or invalid token)
- - 403: Device sync disabled
- - 404: Device not found
-
- **Important:** Kobo devices use query parameter for OPDS catalog
-
- **Use Case:** Kobo devices accessing OPDS catalog
-}
diff --git a/bruno/opds/Get Device Catalog.bru b/bruno/opds/Get Device Catalog.bru
deleted file mode 100644
index aa9ab15..0000000
--- a/bruno/opds/Get Device Catalog.bru
+++ /dev/null
@@ -1,11 +0,0 @@
-meta {
- name: Get Device Catalog
- type: http
- seq: 1
-}
-
-get {
- url: {{opds_base_url}}/devices/{{device_id}}/catalog?page=1&per_page=50
-}
-
-vars:device_id, opds_base_url
diff --git a/bruno/opds/Get Device Navigation.bru b/bruno/opds/Get Device Navigation.bru
deleted file mode 100644
index 4a7e1be..0000000
--- a/bruno/opds/Get Device Navigation.bru
+++ /dev/null
@@ -1,11 +0,0 @@
-meta {
- name: Get Device Navigation
- type: http
- seq: 6
-}
-
-get {
- url: {{opds_base_url}}/devices/{{device_id}}/nav
-}
-
-vars:device_id, opds_base_url
diff --git a/bruno/opds/List Formats.bru b/bruno/opds/List Formats.bru
deleted file mode 100644
index 328f2ad..0000000
--- a/bruno/opds/List Formats.bru
+++ /dev/null
@@ -1,11 +0,0 @@
-meta {
- name: List Formats
- type: http
- seq: 7
-}
-
-get {
- url: {{opds_base_url}}/devices/{{device_id}}/formats/{{book_id}}
-}
-
-vars:device_id, book_id, opds_base_url
diff --git a/bruno/opds/Search Device Catalog.bru b/bruno/opds/Search Device Catalog.bru
deleted file mode 100644
index ba1a640..0000000
--- a/bruno/opds/Search Device Catalog.bru
+++ /dev/null
@@ -1,11 +0,0 @@
-meta {
- name: Search Device Catalog
- type: http
- seq: 2
-}
-
-get {
- url: {{opds_base_url}}/devices/{{device_id}}/search?q=hobbit
-}
-
-vars:device_id, opds_base_url
diff --git a/bruno/progress/Delete Reading Progress.bru b/bruno/progress/Delete Reading Progress.bru
deleted file mode 100644
index 74d4502..0000000
--- a/bruno/progress/Delete Reading Progress.bru
+++ /dev/null
@@ -1,59 +0,0 @@
-meta {
- name: Delete Reading Progress
- type: http
- seq: 1
-}
-
-delete {
- url: {{base_url}}/api/media-items/{{media_item_id}}/progress
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-tests {
- test_delete_reading_progress_success(status, headers, body) {
- if (status !== 200 && status !== 204) {
- throw new Error("Expected status 200 or 204, got " + status);
- }
-
- return true;
- }
-}
-
-vars:pre-request {
- mediaItemId: "8821b703-1234-5678-9123-446655440001"
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Reading Progress
-
- Deletes reading progress for a media item. This is a legacy endpoint - consider using universal progress endpoints instead.
-
- **Method:** DELETE
-
- **Endpoint:** /api/media-items/:id/progress
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string, required): Media item UUID
-
- **Response:** Success message or empty
-
- **Status Codes:**
- - 200: Success
- - 204: Success (no content)
- - 401: Unauthorized
- - 404: Media item not found
- - 500: Internal server error
-
- **Note:** This is a legacy endpoint. Use `/api/progress/:id` endpoints for new implementations.
-}
diff --git a/bruno/progress/Get Reading Progress.bru b/bruno/progress/Get Reading Progress.bru
deleted file mode 100644
index 2c1ae90..0000000
--- a/bruno/progress/Get Reading Progress.bru
+++ /dev/null
@@ -1,37 +0,0 @@
-meta {
- name: Get Reading Progress
- type: http
- seq: 6
-}
-
-get {
- url: {{base_url}}/api/media-items/{{media_item_id}}/progress
- body: none
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Reading Progress
-
- Retrieves the authenticated user's reading progress for a media item.
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string): Media Item UUID
-
- **Response:**
- - `id` (string): Media Item UUID
- - `user_id` (string): User UUID
- - `current_page` (number): Current page number
- - `total_pages` (number, nullable): Total pages
- - `last_read_at` (string): Last read timestamp
-
- **Error Responses:**
- - 401: Invalid authentication
-}
diff --git a/bruno/progress/Update Reading Progress.bru b/bruno/progress/Update Reading Progress.bru
deleted file mode 100644
index 966fa61..0000000
--- a/bruno/progress/Update Reading Progress.bru
+++ /dev/null
@@ -1,44 +0,0 @@
-meta {
- name: Update Reading Progress
- type: http
- seq: 7
-}
-
-put {
- url: {{base_url}}/api/media-items/{{media_item_id}}/progress
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "current_page": 45,
- "total_pages": 200
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Reading Progress
-
- Updates the authenticated user's reading progress for a media item.
-
- **Authentication:** Required (Bearer token)
-
- **Path Parameters:**
- - `id` (string): Media Item UUID
-
- **Request Body:**
- - `current_page` (number, required): Current page number
- - `total_pages` (number, optional): Total pages in book
-
- **Response:** Updated progress object
-
- **Error Responses:**
- - 401: Invalid authentication
- - 400: Invalid request data
-}
diff --git a/bruno/queue/Clear All Queue Items.bru b/bruno/queue/Clear All Queue Items.bru
deleted file mode 100644
index e69de29..0000000
diff --git a/bruno/queue/Clear Device Queue.bru b/bruno/queue/Clear Device Queue.bru
deleted file mode 100644
index 88026e8..0000000
--- a/bruno/queue/Clear Device Queue.bru
+++ /dev/null
@@ -1,43 +0,0 @@
-meta {
- name: Clear Device Queue
- type: http
- seq: 6
-}
-
-delete {
- url: {{base_url}}/api/queue/devices/{{device_id}}/clear
- body: none
- auth: inherit
-}
-
-docs {
- ## Clear Device Queue
-
- Clears all queue items for a specific device.
-
- **Method:** DELETE
-
- **Endpoint:** /api/queue/devices/:device_id/clear
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
-
- **Response:**
- - Success message with count of cleared items
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Device not found
-
- **Example Response:**
- ```json
- {
- "message": "Queue cleared",
- "device_id": "uuid",
- "cleared_count": 10
- }
- ```
-}
diff --git a/bruno/queue/Delete Queue Item.bru b/bruno/queue/Delete Queue Item.bru
deleted file mode 100644
index c8144fb..0000000
--- a/bruno/queue/Delete Queue Item.bru
+++ /dev/null
@@ -1,42 +0,0 @@
-meta {
- name: Delete Queue Item
- type: http
- seq: 5
-}
-
-delete {
- url: {{base_url}}/api/queue/items/{{item_id}}
- body: none
- auth: inherit
-}
-
-docs {
- ## Delete Queue Item
-
- Deletes a queue item from the sync queue.
-
- **Method:** DELETE
-
- **Endpoint:** /api/queue/items/:item_id
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `item_id` (string): Queue item UUID
-
- **Response:**
- - Success message confirming deletion
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Queue item not found
-
- **Example Response:**
- ```json
- {
- "message": "Queue item deleted",
- "item_id": "uuid"
- }
- ```
-}
diff --git a/bruno/queue/Filter by Status - Pending.bru b/bruno/queue/Filter by Status - Pending.bru
deleted file mode 100644
index 5e51d95..0000000
--- a/bruno/queue/Filter by Status - Pending.bru
+++ /dev/null
@@ -1,71 +0,0 @@
-meta {
- name: Filter by Status - Pending
- type: http
- seq: 3
-}
-
-docs {
- Filter queue items by status - show only pending items.
-
- **Endpoint**: GET /queue/items?status=pending
- **Auth**: Required (Bearer token)
-
- ## Query Parameters
-
- | Parameter | Type | Required | Description |
- |-----------|------|-----------|-------------|
- | status | string | Yes | Status filter: pending, processing, completed, failed |
-
- ## Response Fields
-
- | Field | Type | Description |
- |-------|------|-------------|
- | items | array | List of queue items with pending status |
- | total | int | Total matching items |
-
- ## Example Request
-
- ```
- GET /queue/items?status=pending
- ```
-
- ## Example Response
-
- ```json
- {
- "items": [...],
- "total": 15
- }
- ```
-
- ## Error Responses
-
- | Code | Description |
- |------|-------------|
- | 401 | Unauthorized |
- | 500 | Internal server error |
-
- ## Notes
-
- - Only returns items with the specified status
-}
-
-get {
- url: {{base_url}}/queue/items?status=pending
- body: none
- auth: inherit
-}
-
- token: {{jwt_token}}
-}
-
-tests {
- test("status must be 200", function() {
- expect(res.status).to.eql(200);
- });
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
diff --git a/bruno/queue/Get Device Queue Items.bru b/bruno/queue/Get Device Queue Items.bru
deleted file mode 100644
index e69de29..0000000
diff --git a/bruno/queue/Get Device Queue Stats.bru b/bruno/queue/Get Device Queue Stats.bru
deleted file mode 100644
index c9da7f9..0000000
--- a/bruno/queue/Get Device Queue Stats.bru
+++ /dev/null
@@ -1,45 +0,0 @@
-meta {
- name: Get Device Queue Stats
- type: http
- seq: 2
-}
-
-get {
- url: {{base_url}}/api/queue/devices/{{device_id}}/stats
- body: none
- auth: inherit
-}
-
-docs {
- ## Get Device Queue Stats
-
- Retrieves statistics for a specific device's sync queue.
-
- **Method:** GET
-
- **Endpoint:** /api/queue/devices/:device_id/stats
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
-
- **Response:**
- - Queue statistics including pending, completed, and failed counts
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Device not found
-
- **Example Response:**
- ```json
- {
- "device_id": "uuid",
- "total_items": 10,
- "pending": 3,
- "completed": 5,
- "failed": 2
- }
- ```
-}
diff --git a/bruno/queue/Get Queue Statistics.bru b/bruno/queue/Get Queue Statistics.bru
deleted file mode 100644
index e69de29..0000000
diff --git a/bruno/queue/List All Queue Items (Admin).bru b/bruno/queue/List All Queue Items (Admin).bru
deleted file mode 100644
index 2b1f1d4..0000000
--- a/bruno/queue/List All Queue Items (Admin).bru
+++ /dev/null
@@ -1,44 +0,0 @@
-meta {
- name: List All Queue Items (Admin)
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/queue/items
- body: none
- auth: inherit
-}
-
-docs {
- ## List All Queue Items (Admin)
-
- Retrieves all queue items across all devices (admin only).
-
- **Method:** GET
-
- **Endpoint:** /api/queue/items
-
- **Authentication:** Bearer token (admin role required)
-
- **Response:**
- - Array of queue items with device and status information
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden - admin role required
-
- **Example Response:**
- ```json
- [
- {
- "id": "uuid",
- "device_id": "uuid",
- "item_type": "progress",
- "status": "pending",
- "created_at": "2024-01-01T00:00:00Z"
- }
- ]
- ```
-}
diff --git a/bruno/queue/List All Queue Items.bru b/bruno/queue/List All Queue Items.bru
deleted file mode 100644
index e69de29..0000000
diff --git a/bruno/queue/List Device Queue Items.bru b/bruno/queue/List Device Queue Items.bru
deleted file mode 100644
index b2f7cfa..0000000
--- a/bruno/queue/List Device Queue Items.bru
+++ /dev/null
@@ -1,49 +0,0 @@
-meta {
- name: List Device Queue Items
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/queue/devices/{{device_id}}/items
- body: none
- auth: inherit
-}
-
-docs {
- ## List Device Queue Items
-
- Retrieves all queue items for a specific device.
-
- **Method:** GET
-
- **Endpoint:** /api/queue/devices/:device_id/items
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
-
- **Response:**
- - Array of queue items for the specified device
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Device not found
-
- **Example Response:**
- ```json
- [
- {
- "id": "uuid",
- "device_id": "uuid",
- "item_type": "progress",
- "status": "pending",
- "data": {},
- "created_at": "2024-01-01T00:00:00Z",
- "updated_at": "2024-01-01T00:00:00Z"
- }
- ]
- ```
-}
diff --git a/bruno/queue/List Queue Items (Pagination).bru b/bruno/queue/List Queue Items (Pagination).bru
deleted file mode 100644
index 18f7473..0000000
--- a/bruno/queue/List Queue Items (Pagination).bru
+++ /dev/null
@@ -1,65 +0,0 @@
-meta {
- name: List Queue Items (With Pagination)
- type: http
- seq: 2
-}
-
-docs {
- List items in the sync queue with pagination.
-
- **Endpoint**: GET /queue/items
- **Auth**: Required (Bearer token)
-
- ## Query Parameters
-
- | Parameter | Type | Required | Description |
- |-----------|------|-----------|-------------|
- | limit | int | No | Items per page |
- | offset | int | No | Pagination offset |
-
- ## Response Fields
-
- | Field | Type | Description |
- |-------|------|-------------|
- | items | array | List of queue items |
- | total | int | Total number of items |
- | page | int | Current page number |
- | per_page | int | Items per page |
-
- ## Example Request
-
- ```
- GET /queue/items?limit=50&offset=0
- ```
-
- ## Error Responses
-
- | Code | Description |
- |------|-------------|
- | 401 | Unauthorized |
- | 500 | Internal server error |
-
- ## Notes
-
- - Supports pagination for large queue lists
-}
-
-get {
- url: {{base_url}}/queue/items?limit=50&offset=0
- body: none
- auth: inherit
-}
-
- token: {{jwt_token}}
-}
-
-tests {
- test("status must be 200", function() {
- expect(res.status).to.eql(200);
- });
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
diff --git a/bruno/queue/Process Queue Item.bru b/bruno/queue/Process Queue Item.bru
deleted file mode 100644
index e69de29..0000000
diff --git a/bruno/queue/Retry Queue Item.bru b/bruno/queue/Retry Queue Item.bru
deleted file mode 100644
index 6535840..0000000
--- a/bruno/queue/Retry Queue Item.bru
+++ /dev/null
@@ -1,43 +0,0 @@
-meta {
- name: Retry Queue Item
- type: http
- seq: 4
-}
-
-post {
- url: {{base_url}}/api/queue/items/{{item_id}}/retry
- body: none
- auth: inherit
-}
-
-docs {
- ## Retry Queue Item
-
- Retries a failed queue item.
-
- **Method:** POST
-
- **Endpoint:** /api/queue/items/:item_id/retry
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `item_id` (string): Queue item UUID
-
- **Response:**
- - Success message indicating retry initiated
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Queue item not found
- - 400: Invalid item status
-
- **Example Response:**
- ```json
- {
- "message": "Queue item retry initiated",
- "item_id": "uuid"
- }
- ```
-}
diff --git a/bruno/queue/api.bru b/bruno/queue/api.bru
deleted file mode 100644
index e77d66b..0000000
--- a/bruno/queue/api.bru
+++ /dev/null
@@ -1,85 +0,0 @@
-meta {
- name: "Bookhoard Sync Queue API"
- type: "collection"
- environment: {
- development: {
- base_url: "http://localhost:8765/api"
- },
- production: {
- base_url: "https://your-domain.com/api"
- }
- }
-}
-
-# List Queue Items
-@name("List All Queue Items")
-GET {{environment.base_url}}/queue/items
-Authorization: Bearer {{jwt_token}}
-
-@name("List Queue Items - With Pagination")
-GET {{environment.base_url}}/queue/items?limit=50&offset=0
-Authorization: Bearer {{jwt_token}}
-
-@name("Filter by Status - Pending")
-GET {{environment.base_url}}/queue/items?status=pending
-Authorization: Bearer {{jwt_token}}
-
-@name("Filter by Status - Failed")
-GET {{environment.base_url}}/queue/items?status=failed
-Authorization: Bearer {{jwt_token}}
-
-@name("Filter by Status - Completed")
-GET {{environment.base_url}}/queue/items?status=completed
-Authorization: Bearer {{jwt_token}}
-
-@name("Filter by Type - Progress")
-GET {{environment.base_url}}/queue/items?status=all&type=progress
-Authorization: Bearer {{jwt_token}}
-
-@name("Filter by Type - Notes")
-GET {{environment.base_url}}/queue/items?status=all&type=note
-Authorization: Bearer {{jwt_token}}
-
-@name("Filter by Type - Highlights")
-GET {{environment.base_url}}/queue/items?status=all&type=highlight
-Authorization: Bearer {{jwt_token}}
-
-@name("Filter by Type - Bookmarks")
-GET {{environment.base_url}}/queue/items?status=all&type=bookmark
-Authorization: Bearer {{jwt_token}}
-
-# Process Queue Item
-@name("Process Queue Item")
-POST {{environment.base_url}}/queue/items/{{queue_item_id}}/process
-Authorization: Bearer {{jwt_token}}
-
-@name("Retry Queue Item")
-POST {{environment.base_url}}/queue/items/{{queue_item_id}}/retry
-Authorization: Bearer {{jwt_token}}
-
-# Delete Queue Item
-@name("Delete Queue Item")
-DELETE {{environment.base_url}}/queue/items/{{queue_item_id}}
-Authorization: Bearer {{jwt_token}}
-
-# Clear All Queue
-@name("Clear All Queue Items")
-DELETE {{environment.base_url}}/queue/clear
-Authorization: Bearer {{jwt_token}}
-
-@name("Clear Failed Items")
-DELETE {{environment.base_url}}/queue/clear-failed
-Authorization: Bearer {{jwt_token}}
-
-# Get Queue Stats
-@name("Get Queue Statistics")
-GET {{environment.base_url}}/queue/stats
-Authorization: Bearer {{jwt_token}}
-
-@name("Get Device Queue Stats")
-GET {{environment.base_url}}/queue/devices/{{device_id}}/stats
-Authorization: Bearer {{jwt_token}}
-
-@name("Get Device Queue Items")
-GET {{environment.base_url}}/queue/devices/{{device_id}}/items
-Authorization: Bearer {{jwt_token}}
diff --git a/bruno/scanner/Get Scan Status.bru b/bruno/scanner/Get Scan Status.bru
deleted file mode 100644
index 75f88f7..0000000
--- a/bruno/scanner/Get Scan Status.bru
+++ /dev/null
@@ -1,71 +0,0 @@
-meta {
- name: Get Scan Status
- type: http
- seq: 4
-}
-
-get {
- url: {{base_url}}/api/scanner/status/{{job_id}}
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Scan Status
-
- Retrieves the status and progress of an asynchronous scan job.
-
- **Method:** GET
-
- **Endpoint:** /api/scanner/status/:jobId
-
- **Authentication:** Required (Bearer token, Admin only)
-
- **URL Parameters:**
- - `jobId` (string, required): The job ID returned from the scan endpoint
- - Example: `550e8400-e29b-41d4-a716-446655440000`
-
- **Response:**
- ```json
- {
- "job_id": "550e8400-e29b-41d4-a716-446655440000",
- "status": "completed",
- "error": "",
- "result": {
- "message": "scan completed",
- "library_id": "550e8400-e29b-41d4-a716-446655440000"
- },
- "progress": 1.0
- }
- ```
-
- **Properties:**
- - `job_id` (string): Job identifier
- - `status` (string): Current status
- - `pending`: Job is queued
- - `running`: Job is currently processing
- - `completed`: Job finished successfully
- - `failed`: Job failed with error
- - `cancelled`: Job was cancelled
- - `error` (string): Error message if status is "failed"
- - `result` (object): Scan results when completed
- - `message` (string): Completion message
- - `library_id` (string): Library that was scanned
- - `progress` (number): Progress indicator (0.0 to 1.0)
-
- **Status Codes:**
- - 200: Job status retrieved successfully
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Job not found
-
- **Example Workflow:**
- 1. POST /api/scanner/scan with folder_paths
- 2. Receive job_id in response
- 3. Poll GET /api/scanner/status/{job_id} every few seconds
- 4. When status is "completed" or "failed", stop polling
-}
diff --git a/bruno/scanner/Get Watch Mode Status.bru b/bruno/scanner/Get Watch Mode Status.bru
deleted file mode 100644
index 75ec851..0000000
--- a/bruno/scanner/Get Watch Mode Status.bru
+++ /dev/null
@@ -1,47 +0,0 @@
-meta {
- name: Get Watch Mode Status
- type: http
- seq: 7
-}
-
-get {
- url: {{base_url}}/api/scanner/watch/status
- auth: inherit
-}
-
-docs {
- ## Get Watch Mode Status
-
- Retrieves the current status of watch mode for all libraries.
-
- **Method:** GET
-
- **Endpoint:** /api/scanner/watch/status
-
- **Authentication:** Required (Bearer token, Admin only)
-
- **Response:** HTTP 200 (OK)
- ```json
- {
- "watching_libraries": [
- "550e8400-e29b-41d4-a716-446655440000",
- "660e8400-e29b-41d4-a716-446655440001"
- ],
- "total_watching": 2
- }
- ```
-
- **Properties:**
- - `watching_libraries` (array of strings): List of library IDs currently being watched
- - `total_watching` (number): Total number of libraries being watched
-
- **Status Codes:**
- - 200: Status retrieved successfully
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
-
- **Use cases:**
- - Check which libraries are currently being monitored
- - Verify that watch mode started successfully after server boot
- - Debug file system monitoring issues
-}
diff --git a/bruno/scanner/Scan Media Items.bru b/bruno/scanner/Scan Media Items.bru
deleted file mode 100644
index 89e8428..0000000
--- a/bruno/scanner/Scan Media Items.bru
+++ /dev/null
@@ -1,62 +0,0 @@
-meta {
- name: Scan Media Items (Background)
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/scanner/scan
- body: json
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-body:json {
- {
- "folder_paths": ["/path/to/media"]
- }
-}
-
-docs {
- ## Scan Media Items (Background)
-
- Triggers an asynchronous media items scanning operation. The scan runs in the background and can be monitored using the job ID.
-
- **Method:** POST
-
- **Endpoint:** /api/scanner/scan
-
- **Authentication:** Required (Bearer token, Admin only)
-
- **Request Body:**
- - `folder_paths` (array of strings, required): List of folder paths to scan
- - Example: `["/path/to/media", "/another/path"]`
-
- **Response:** HTTP 202 (Accepted)
- ```json
- {
- "message": "scan job enqueued",
- "job_id": "550e8400-e29b-41d4-a716-446655440000",
- "status": "pending"
- }
- ```
-
- **Properties:**
- - `message` (string): Confirmation message
- - `job_id` (string): Unique job identifier for tracking progress
- - `status` (string): Initial job status ("pending")
-
- **Status Codes:**
- - 202: Scan job successfully enqueued
- - 400: Invalid request (missing folder_paths)
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 500: Failed to enqueue scan job
-
- **Next Steps:**
- Use the returned `job_id` with `GET /api/scanner/status/:jobId` to check scan progress.
-}
diff --git a/bruno/scanner/Start Scanner.bru b/bruno/scanner/Start Scanner.bru
deleted file mode 100644
index c31d8f7..0000000
--- a/bruno/scanner/Start Scanner.bru
+++ /dev/null
@@ -1,40 +0,0 @@
-meta {
- name: Start Scanner
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/scanner/start
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Start Scanner
-
- Starts the media scanner service for indexing library content.
-
- **Method:** POST
-
- **Endpoint:** /api/scanner/start
-
- **Authentication:** Required (Bearer token)
-
- **Response:**
- - JSON object containing scanner status
- - `status` (string): Scanner state ("started", "running")
- - `message` (string): Status message
- - `started_at` (string): Timestamp when scanner started
-
- **Status Codes:**
- - 200: Scanner started successfully
- - 401: Unauthorized
- - 403: Forbidden (insufficient permissions)
- - 409: Scanner already running
- - 500: Internal server error
-}
\ No newline at end of file
diff --git a/bruno/scanner/Start Watch Mode.bru b/bruno/scanner/Start Watch Mode.bru
deleted file mode 100644
index f38202a..0000000
--- a/bruno/scanner/Start Watch Mode.bru
+++ /dev/null
@@ -1,61 +0,0 @@
-meta {
- name: Start Watch Mode
- type: http
- seq: 5
-}
-
-post {
- url: {{base_url}}/api/scanner/watch/start
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "library_id": "{{library_id}}"
- }
-}
-
-docs {
- ## Start Watch Mode
-
- Starts real-time file system monitoring for a specific library. New media items will be detected and processed almost instantly.
-
- **Method:** POST
-
- **Endpoint:** /api/scanner/watch/start
-
- **Authentication:** Required (Bearer token, Admin only)
-
- **Request Body:**
- - `library_id` (string, required): UUID of the library to watch
- - Example: `"550e8400-e29b-41d4-a716-446655440000"`
-
- **Response:** HTTP 200 (OK)
- ```json
- {
- "message": "watch mode started for library",
- "library_id": "550e8400-e29b-41d4-a716-446655440000"
- }
- ```
-
- **How it works:**
- - Monitors all folders configured for the library
- - Automatically detects new media files (CREATE events)
- - Detects modifications to existing files (WRITE events)
- - Processes new files within milliseconds of detection
- - Automatically watches new subdirectories as they're created
-
- **Supported file types:**
- - `.epub`, `.pdf`, `.mobi`, `.azw3`, `.fb2`, `.txt`
-
- **Status Codes:**
- - 200: Watch mode started successfully
- - 400: Invalid request (missing library_id)
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 409: Already watching this library
- - 500: Failed to start watch mode (no folders configured, etc.)
-
- **Note:** Watch mode is automatically started for all libraries when the server starts.
-}
diff --git a/bruno/scanner/Stop Scanner.bru b/bruno/scanner/Stop Scanner.bru
deleted file mode 100644
index 1c6e2c1..0000000
--- a/bruno/scanner/Stop Scanner.bru
+++ /dev/null
@@ -1,40 +0,0 @@
-meta {
- name: Stop Scanner
- type: http
- seq: 3
-}
-
-post {
- url: {{base_url}}/api/scanner/stop
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Stop Scanner
-
- Stops the currently running media scanner service.
-
- **Method:** POST
-
- **Endpoint:** /api/scanner/stop
-
- **Authentication:** Required (Bearer token)
-
- **Response:**
- - JSON object containing scanner status
- - `status` (string): Scanner state ("stopped", "idle")
- - `message` (string): Status message
- - `stopped_at` (string): Timestamp when scanner stopped
-
- **Status Codes:**
- - 200: Scanner stopped successfully
- - 401: Unauthorized
- - 403: Forbidden (insufficient permissions)
- - 409: Scanner not running
- - 500: Internal server error
-}
\ No newline at end of file
diff --git a/bruno/scanner/Stop Watch Mode.bru b/bruno/scanner/Stop Watch Mode.bru
deleted file mode 100644
index 51420c2..0000000
--- a/bruno/scanner/Stop Watch Mode.bru
+++ /dev/null
@@ -1,51 +0,0 @@
-meta {
- name: Stop Watch Mode
- type: http
- seq: 6
-}
-
-post {
- url: {{base_url}}/api/scanner/watch/stop
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "library_id": "{{library_id}}"
- }
-}
-
-docs {
- ## Stop Watch Mode
-
- Stops real-time file system monitoring for a specific library.
-
- **Method:** POST
-
- **Endpoint:** /api/scanner/watch/stop
-
- **Authentication:** Required (Bearer token, Admin only)
-
- **Request Body:**
- - `library_id` (string, required): UUID of the library to stop watching
- - Example: `"550e8400-e29b-41d4-a716-446655440000"`
-
- **Response:** HTTP 200 (OK)
- ```json
- {
- "message": "watch mode stopped for library",
- "library_id": "550e8400-e29b-41d4-a716-446655440000"
- }
- ```
-
- **Status Codes:**
- - 200: Watch mode stopped successfully
- - 400: Invalid request (missing library_id)
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 404: Not watching this library
- - 500: Internal server error
-
- **Note:** If no libraries are being watched, the watch mode context is cleaned up automatically.
-}
diff --git a/bruno/sidecar/Download Device Sidecar File.bru b/bruno/sidecar/Download Device Sidecar File.bru
deleted file mode 100644
index c8db7eb..0000000
--- a/bruno/sidecar/Download Device Sidecar File.bru
+++ /dev/null
@@ -1,61 +0,0 @@
-meta {
- name: Download Device Sidecar File
- type: http
- seq: 2
-}
-
-get {
- url: {{base_url}}/api/devices/{{device_id}}/sidecar/download
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{user_token}}
- Content-Type: application/json
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const headers = res.getHeaders();
- const contentType = headers.get("Content-Type");
- const contentDisposition = headers.get("Content-Disposition");
- tests('Content-Type is JSON', contentType && contentType.includes("application/json"));
- tests('Has .bookhoard.json filename', contentDisposition && contentDisposition.includes(".bookhoard.json"));
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Download Device Sidecar File
-
- Downloads the sidecar configuration file for a device in JSON format.
-
- **Method:** GET
-
- **Endpoint:** /api/devices/{device_id}/sidecar/download
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
-
- **Response Headers:**
- - `Content-Type`: application/json
- - `Content-Disposition`: attachment; filename="device.bookhoard.json"
-
- **Response Body:** JSON sidecar configuration file
-
- **Status Codes:**
- - 200: Success - file returned
- - 401: Unauthorized
- - 404: Device not found
- - 500: Internal server error
-}
diff --git a/bruno/sidecar/Get Device Sidecar Config.bru b/bruno/sidecar/Get Device Sidecar Config.bru
deleted file mode 100644
index 5adaa15..0000000
--- a/bruno/sidecar/Get Device Sidecar Config.bru
+++ /dev/null
@@ -1,61 +0,0 @@
-meta {
- name: Get Device Sidecar Config
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/devices/{{device_id}}/sidecar
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{user_token}}
- Content-Type: application/json
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests('Version is 1.0', body.version === "1.0");
- tests('Has bookhoard config', body.bookhoard !== undefined);
- tests('Has books array', body.books !== undefined);
- tests('Has collections array', body.collections !== undefined);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Device Sidecar Config
-
- Retrieves sidecar configuration for a specific device.
-
- **Method:** GET
-
- **Endpoint:** /api/devices/{device_id}/sidecar
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `device_id` (string): Device UUID
-
- **Response:**
- - `version` (string): Sidecar version (e.g., "1.0")
- - `bookhoard` (object): Bookhoard configuration
- - `books` (array): Array of book configurations
- - `collections` (array): Array of collection configurations
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Device not found
- - 500: Internal server error
-}
diff --git a/bruno/sidecar/Get System Configuration.bru b/bruno/sidecar/Get System Configuration.bru
deleted file mode 100644
index 0b1b51a..0000000
--- a/bruno/sidecar/Get System Configuration.bru
+++ /dev/null
@@ -1,57 +0,0 @@
-meta {
- name: Get System Configuration
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/system/config
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{admin_token}}
- Content-Type: application/json
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests('Has base_url', body.base_url !== undefined);
- tests('Has opds_base_url', body.opds_base_url !== undefined);
- tests('Has api_base_url', body.api_base_url !== undefined);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get System Configuration
-
- Retrieves system-wide configuration settings.
-
- **Method:** GET
-
- **Endpoint:** /api/system/config
-
- **Authentication:** Bearer token (admin only)
-
- **Response:**
- - `base_url` (string): Base URL
- - `opds_base_url` (string): OPDS endpoint URL
- - `api_base_url` (string): API endpoint URL
- - Additional configuration fields
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (admin only)
- - 500: Internal server error
-}
diff --git a/bruno/sidecar/Update System Configuration.bru b/bruno/sidecar/Update System Configuration.bru
deleted file mode 100644
index 7703c4d..0000000
--- a/bruno/sidecar/Update System Configuration.bru
+++ /dev/null
@@ -1,67 +0,0 @@
-meta {
- name: Update System Configuration
- type: http
- seq: 4
-}
-
-put {
- url: {{base_url}}/api/system/config
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{admin_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "base_url": "https://bookhoard.example.com",
- "opds_base_url": "https://bookhoard.example.com/opds",
- "api_base_url": "https://bookhoard.example.com/api"
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests('Status is success', body.status === "success");
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update System Configuration
-
- Updates system-wide configuration settings for the Bookhoard instance.
-
- **Method:** PUT
-
- **Endpoint:** /api/system/config
-
- **Authentication:** Bearer token (admin only)
-
- **Request Body:**
- - `base_url` (string): Base URL for the instance
- - `opds_base_url` (string): OPDS endpoint base URL
- - `api_base_url` (string): API endpoint base URL
-
- **Response:**
- - `status` (string): Update status
- - `config` (object): Updated configuration
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid configuration
- - 401: Unauthorized
- - 403: Forbidden (admin only)
- - 500: Internal server error
-}
diff --git a/bruno/sync-kobo/Auto Link Books.bru b/bruno/sync-kobo/Auto Link Books.bru
deleted file mode 100644
index 484dae2..0000000
--- a/bruno/sync-kobo/Auto Link Books.bru
+++ /dev/null
@@ -1,58 +0,0 @@
-meta {
- name: Auto-Link Unlinked Books
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/sync/auto-link-books
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "confidence_threshold": 0.8,
- "limit": 50
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Auto-Link Unlinked Books
-
- Automatically links unlinked books to media items based on title and author matching with a configurable confidence threshold.
-
- **Method:** POST
-
- **Endpoint:** /sync/auto-link-books
-
- **Authentication:** Bearer token
-
- **Request Body:**
- - `confidence_threshold` (number, optional): Minimum confidence score for auto-linking (0-1, default: 0.8)
- - `limit` (number, optional): Maximum number of books to auto-link (default: 50)
-
- **Response:**
- - `results` (array): Results for each auto-link attempt
- - `total` (number): Total number of books processed
- - `success` (number): Number of successful links
- - `failed` (number): Number of failed links
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request data
- - 401: Unauthorized
- - 500: Internal server error
-
- **Note:** Higher confidence thresholds produce fewer but more accurate matches. Consider the tradeoff between automation and accuracy.
-}
diff --git a/bruno/sync-kobo/Bulk Link Books.bru b/bruno/sync-kobo/Bulk Link Books.bru
deleted file mode 100644
index ad14e51..0000000
--- a/bruno/sync-kobo/Bulk Link Books.bru
+++ /dev/null
@@ -1,70 +0,0 @@
-meta {
- name: Bulk Link Unlinked Books
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/sync/bulk-link-books
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
- Authorization: Bearer {{authToken}}
-}
-
-body:json {
- {
- "links": [
- {
- "unlinked_book_id": "{{unlinkedBookId1}}",
- "media_item_id": "{{mediaItemId1}}",
- "confidence_score": 1.0
- },
- {
- "unlinked_book_id": "{{unlinkedBookId2}}",
- "media_item_id": "{{mediaItemId2}}",
- "confidence_score": 0.9
- }
- ]
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Bulk Link Unlinked Books
-
- Links multiple unlinked books to media items in a single request.
-
- **Method:** POST
-
- **Endpoint:** /sync/bulk-link-books
-
- **Authentication:** Bearer token
-
- **Request Body:**
- - `links` (array): Array of link objects
- - `unlinked_book_id` (string): Unlinked book UUID
- - `media_item_id` (string): Media item UUID to link to
- - `confidence_score` (number): Match confidence (0-1)
-
- **Response:**
- - `results` (array): Results for each link attempt
- - `total` (number): Total number of links processed
- - `success` (number): Number of successful links
- - `failed` (number): Number of failed links
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request data
- - 401: Unauthorized
- - 500: Internal server error
-
- **Note:** Use this endpoint after reviewing suggestions from the Get Unlinked Book Suggestions endpoint.
-}
diff --git a/bruno/sync-kobo/Get Unlinked Book Suggestions.bru b/bruno/sync-kobo/Get Unlinked Book Suggestions.bru
deleted file mode 100644
index bb351c8..0000000
--- a/bruno/sync-kobo/Get Unlinked Book Suggestions.bru
+++ /dev/null
@@ -1,50 +0,0 @@
-meta {
- name: Get Unlinked Book Suggestions
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/sync/unlinked-books/{{unlinkedBookId}}/suggestions
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{authToken}}
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Unlinked Book Suggestions
-
- Retrieves suggested media items from the library that match an unlinked book, enabling manual linking.
-
- **Method:** GET
-
- **Endpoint:** /sync/unlinked-books/{unlinkedBookId}/suggestions
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `unlinkedBookId` (string): Unlinked book UUID
-
- **Response:**
- - Array of suggested media items with:
- - `id` (string): Media item UUID
- - `title` (string): Media item title
- - `author` (string): Media item author
- - `confidence_score` (number): Match confidence (0-1)
- - `match_reasons` (array): Reasons for the suggestion
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Unlinked book not found
-
- **Note:** Suggestions are generated based on title similarity, author matching, and other metadata comparisons.
-}
diff --git a/bruno/sync-kobo/api.bru b/bruno/sync-kobo/api.bru
deleted file mode 100644
index 2e36980..0000000
--- a/bruno/sync-kobo/api.bru
+++ /dev/null
@@ -1,230 +0,0 @@
-meta {
- name: "Bookhoard Kobo Sync API"
- type: "collection"
- environment: {
- development: {
- base_url: "http://localhost:8765/api"
- },
- production: {
- base_url: "https://your-domain.com/api"
- }
- }
-}
-
-# Authentication Headers
-@name("Kobo Device Token")
-@AuthorizationHeader({
- name: "Authorization",
- value: "Bearer {{kobo_device_token}}"
-})
-{
- "token": "{{kobo_device_token}}"
-}
-
-@name("Kobo Device Info Header")
-@Request({
- name: "x-kobo-device",
- value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara","SerialNumber":"{{kobo_serial}}"}'
-})
-{
- "test": "data"
-}
-
-# Sync Markup (Reading Progress + Annotations)
-@name("Sync Reading Progress")
-POST {{environment.base_url}}/sync/kobo/markup
-Authorization: Bearer {{kobo_device_token}}
-x-kobo-device: {"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}
-Content-Type: application/json
-{
- "ReadingSync": [
- {
- "ContentId": "book-uuid-here",
- "PercentRead": 45.6,
- "EntitlementId": "entitlement-id-here",
- "RemainingTimeMinutes": 120,
- "FirstReadTime": "2026-01-25T10:00:00Z",
- "LastModified": "2026-01-30T20:00:00Z"
- }
- ],
- "BookmarkSync": []
-}
-
-@name("Sync Progress with Bookmarks")
-POST {{environment.base_url}}/sync/kobo/markup
-Authorization: Bearer {{kobo_device_token}}
-x-kobo-device: {"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}
-Content-Type: application/json
-{
- "ReadingSync": [
- {
- "ContentId": "book-uuid",
- "PercentRead": 42.3,
- "EntitlementId": "entitlement-id",
- "RemainingTimeMinutes": 138,
- "FirstReadTime": "2026-01-25T10:00:00Z",
- "LastModified": "2026-01-30T20:00:00Z"
- }
- ],
- "BookmarkSync": [
- {
- "ContentId": "book-uuid",
- "BookmarkText": "highlighted text passage",
- "BookmarkType": "annotation",
- "BookmarkTitle": "Chapter 3"
- }
- ]
-}
-
-@name("Sync Multiple Books Progress")
-POST {{environment.base_url}}/sync/kobo/markup
-Authorization: Bearer {{kobo_device_token}}
-x-kobo-device: {"DeviceId":"{{kobo_device_id}}","Model":"Kobo Aura"}
-Content-Type: application/json
-{
- "ReadingSync": [
- {
- "ContentId": "book-1-uuid",
- "PercentRead": 25.0,
- "EntitlementId": "entitlement-1",
- "RemainingTimeMinutes": 240,
- "FirstReadTime": "2026-01-25T10:00:00Z",
- "LastModified": "2026-01-30T18:00:00Z"
- },
- {
- "ContentId": "book-2-uuid",
- "PercentRead": 78.5,
- "EntitlementId": "entitlement-2",
- "RemainingTimeMinutes": 45,
- "FirstReadTime": "2026-01-25T14:00:00Z",
- "LastModified": "2026-01-30T20:00:00Z"
- }
- ],
- "BookmarkSync": []
-}
-
-@name("Sync with Annotations")
-POST {{environment.base_url}}/sync/kobo/markup
-Authorization: Bearer {{kobo_device_token}}
-x-kobo-device: {"DeviceId":"{{kobo_device_id}}","Model":"Kobo Libra"}
-Content-Type: application/json
-{
- "ReadingSync": [
- {
- "ContentId": "book-uuid",
- "PercentRead": 55.0,
- "EntitlementId": "entitlement-id",
- "RemainingTimeMinutes": 120,
- "FirstReadTime": "2026-01-25T10:00:00Z",
- "LastModified": "2026-01-30T20:00:00Z"
- }
- ],
- "BookmarkSync": [
- {
- "ContentId": "book-uuid",
- "BookmarkText": "Important quote",
- "BookmarkType": "annotation",
- "BookmarkTitle": "Chapter 4 - The Truth"
- },
- {
- "ContentId": "book-uuid",
- "BookmarkText": "Another quote",
- "BookmarkType": "annotation",
- "BookmarkTitle": "Chapter 5"
- },
- {
- "ContentId": "book-uuid",
- "BookmarkText": "Note to myself",
- "BookmarkType": "note",
- "BookmarkTitle": "Personal note"
- }
- ]
-}
-
-# Sync Bookmark
-@name("Sync Single Bookmark")
-POST {{environment.base_url}}/sync/kobo/bookmark
-Authorization: Bearer {{kobo_device_token}}
-x-kobo-device: {"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}
-Content-Type: application/json
-{
- "ContentId": "book-uuid",
- "BookmarkText": "Bookmarked passage",
- "BookmarkType": "annotation",
- "BookmarkTitle": "Chapter 3"
-}
-
-# Get Library
-@name("Get Kobo Library")
-GET {{environment.base_url}}/sync/kobo/library
-Authorization: Bearer {{kobo_device_token}}
-
-@name("Get Library for Book")
-GET {{environment.base_url}}/sync/kobo/library
-Authorization: Bearer {{kobo_device_token}}
-Content-Type: application/json
-{
- "ContentId": "book-uuid"
-}
-
-# Analytics Get Tests
-@name("Get Analytics Tests")
-POST {{environment.base_url}}/sync/kobo/v1/analytics/gettests
-Authorization: Bearer {{kobo_device_token}}
-Content-Type: application/json
-{
- "platform": "android"
- "firmware_version": "4.30.19023"
-}
-
-# Initialization
-@name("Kobo Initialization")
-GET {{environment.base_url}}/sync/kobo/v1/initialization
-Authorization: Bearer {{kobo_device_token}}
-
-@name("Initialization with Device Info")
-GET {{environment.base_url}}/sync/kobo/v1/initialization?Platform=android&FirmwareVersion=4.30.19023
-Authorization: Bearer {{kobo_device_token}}
-
-# Sync from Server
-@name("Sync Books from Server to Kobo")
-POST {{environment.base_url}}/sync/kobo/sync-from-server
-Authorization: Bearer {{kobo_device_token}}
-Content-Type: application/json
-{
- "force_sync": true,
- "books": ["book-uuid-1", "book-uuid-2"]
-}
-
-@name("Sync Specific Book from Server")
-POST {{environment.base_url}}/sync/kobo/sync-from-server
-Authorization: Bearer {{kobo_device_token}}
-Content-Type: application/json
-{
- "ContentId": "book-uuid",
- "EntitlementId": "entitlement-id"
-}
-
-@name("Partial Sync - Since Date")
-POST {{environment.base_url}}/sync/kobo/sync-from-server
-Authorization: Bearer {{kobo_device_token}}
-Content-Type: application/json
-{
- "books": ["book-1", "book-2"],
- "sync_options": {
- "since_date": "2026-01-30T00:00:00Z",
- "include_annotations": true
- }
-}
-
-@name("Sync with Conflict Resolution Preference")
-POST {{environment.base_url}}/sync/kobo/sync-from-server
-Authorization: Bearer {{kobo_device_token}}
-Content-Type: application/json
-{
- "books": ["book-1", "book-2", "book-3"],
- "sync_options": {
- "conflict_resolution": "most_recent",
- "force_annotations": false
- }
-}
diff --git a/bruno/sync-kobo/get-initialization-url-token.bru b/bruno/sync-kobo/get-initialization-url-token.bru
deleted file mode 100644
index e399cfd..0000000
--- a/bruno/sync-kobo/get-initialization-url-token.bru
+++ /dev/null
@@ -1,40 +0,0 @@
-meta {
- name: Kobo Initialization - URL Path Token
- type: http
- seq: 4
-}
-
-get {
- url: {{base_url}}/sync/kobo/{{kobo_device_token}}/v1/initialization
- body: none
- auth: none
-}
-
-docs {
- ## Kobo Initialization - URL Path Token
-
- Returns initialization data for Kobo device using token in URL path.
-
- **Method:** GET
-
- **Endpoint:** /sync/kobo/{kobo_device_token}/v1/initialization
-
- **Authentication:** URL path parameter (Kobo devices never send Bearer tokens)
-
- **Path Parameters:**
- - `kobo_device_token` (string): Device auth token from devices.auth_token
-
- **Response:** Initialization configuration and settings
-
- **Status Codes:**
- - 200: Success (initialization data)
- - 401: Unauthorized
- - 403: Device sync disabled
- - 404: Device not found
-
- **Important:** Kobo devices use URL path token exclusively, never Bearer header
-
- **Use Case:** Kobo devices initializing sync connection via stock firmware
-
- **Note:** This endpoint is called when Kobo first connects to sync server
-}
diff --git a/bruno/sync-kobo/get-library-url-token.bru b/bruno/sync-kobo/get-library-url-token.bru
deleted file mode 100644
index c124cc4..0000000
--- a/bruno/sync-kobo/get-library-url-token.bru
+++ /dev/null
@@ -1,38 +0,0 @@
-meta {
- name: Get Library - URL Path Token
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/sync/kobo/{{kobo_device_token}}/library
- body: none
- auth: none
-}
-
-docs {
- ## Get Kobo Library - URL Path Token
-
- Retrieves library metadata for Kobo device using token in URL path.
-
- **Method:** GET
-
- **Endpoint:** /sync/kobo/{kobo_device_token}/library
-
- **Authentication:** URL path parameter (Kobo devices never send Bearer tokens)
-
- **Path Parameters:**
- - `kobo_device_token` (string): Device auth token from devices.auth_token
-
- **Response:** Library metadata with book list
-
- **Status Codes:**
- - 200: Success (library metadata)
- - 401: Unauthorized
- - 403: Device sync disabled
- - 404: Device not found
-
- **Important:** Kobo devices use URL path token exclusively, never Bearer header
-
- **Use Case:** Kobo devices retrieving library information via stock firmware
-}
diff --git a/bruno/sync-kobo/get-unlinked-books.bru b/bruno/sync-kobo/get-unlinked-books.bru
deleted file mode 100644
index 2aa3a59..0000000
--- a/bruno/sync-kobo/get-unlinked-books.bru
+++ /dev/null
@@ -1,60 +0,0 @@
-meta {
- name: Get Unlinked Books - User View
- type: http
- seq: 4
-}
-
-get {
- url: {{base_url}}/api/sync/unlinked-books
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{user_token}}
- Content-Type: application/json
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests('Has unlinked array', body.unlinked !== undefined);
- tests('Has total count', body.total !== undefined);
- tests('Total >= 0', body.total >= 0);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Unlinked Books - User View
-
- Retrieves all unlinked books for the authenticated user that need manual linking.
-
- **Method:** GET
-
- **Endpoint:** /api/sync/unlinked-books
-
- **Authentication:** Bearer token
-
- **Response:**
- - `unlinked` (array): Array of unlinked book objects
- - `id` (string): Unlinked book UUID
- - `title` (string): Book title
- - `author` (string): Book author
- - `device_id` (string): Source device ID
- - `device_name` (string): Source device name
- - `detected_at` (string): Detection timestamp
- - `total` (number): Total count of unlinked books
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 500: Internal server error
-}
diff --git a/bruno/sync-kobo/link-book.bru b/bruno/sync-kobo/link-book.bru
deleted file mode 100644
index 425c265..0000000
--- a/bruno/sync-kobo/link-book.bru
+++ /dev/null
@@ -1,72 +0,0 @@
-meta {
- name: Link Unlinked Book - Manual Resolution
- type: http
- seq: 5
-}
-
-post {
- url: {{base_url}}/api/sync/link-book
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{user_token}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "unlinked_book_id": "{{unlinked_book_id}}",
- "media_item_id": "{{media_item_id}}",
- "confidence_score": 1.0
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests['Status is linked'] = body.status === "linked";
- tests('Has unlinked_book_id', body.unlinked_book_id !== undefined);
- tests('Has media_item_id', body.media_item_id !== undefined);
- tests('Has message', body.message !== undefined);
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Link Unlinked Book - Manual Resolution
-
- Manually links an unlinked book to a media item in the library.
-
- **Method:** POST
-
- **Endpoint:** /api/sync/link-book
-
- **Authentication:** Bearer token
-
- **Request Body:**
- - `unlinked_book_id` (string): Unlinked book UUID
- - `media_item_id` (string): Media item UUID to link to
- - `confidence_score` (number): Match confidence (0-1, 1.0 for manual)
-
- **Response:**
- - `status` (string): Link status (linked)
- - `unlinked_book_id` (string): Unlinked book UUID
- - `media_item_id` (string): Media item UUID
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success - book linked
- - 400: Invalid request
- - 401: Unauthorized
- - 404: Book or media item not found
- - 500: Internal server error
-}
diff --git a/bruno/sync-kobo/sync-bookmark-url-token.bru b/bruno/sync-kobo/sync-bookmark-url-token.bru
deleted file mode 100644
index 2ad5f09..0000000
--- a/bruno/sync-kobo/sync-bookmark-url-token.bru
+++ /dev/null
@@ -1,57 +0,0 @@
-meta {
- name: Sync Bookmark - URL Path Token
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/sync/kobo/{{kobo_device_token}}/bookmark
- body: json
- auth: none
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "ContentId": "book-uuid",
- "BookmarkText": "Highlighted text",
- "BookmarkType": "annotation",
- "BookmarkTitle": "Chapter 3"
- }
-}
-
-docs {
- ## Sync Bookmark - URL Path Token
-
- Synchronizes bookmarks and annotations from Kobo device using token in URL path.
-
- **Method:** POST
-
- **Endpoint:** /sync/kobo/{kobo_device_token}/bookmark
-
- **Authentication:** URL path parameter (Kobo devices never send Bearer tokens)
-
- **Path Parameters:**
- - `kobo_device_token` (string): Device auth token from devices.auth_token
-
- **Request Body:**
- - `ContentId` (string): Book UUID
- - `BookmarkText` (string): Highlighted or annotated text
- - `BookmarkType` (string): Type (annotation, bookmark, note)
- - `BookmarkTitle` (string): Title for the bookmark
-
- **Response:** Success message
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Device sync disabled
- - 500: Internal server error
-
- **Important:** Kobo devices use URL path token exclusively, never Bearer header
-
- **Use Case:** Kobo devices syncing bookmarks and highlights via stock firmware
-}
diff --git a/bruno/sync-kobo/sync-markup-url-token.bru b/bruno/sync-kobo/sync-markup-url-token.bru
deleted file mode 100644
index 6af2762..0000000
--- a/bruno/sync-kobo/sync-markup-url-token.bru
+++ /dev/null
@@ -1,62 +0,0 @@
-meta {
- name: Sync Markup - URL Path Token
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/sync/kobo/{{kobo_device_token}}/markup
- body: json
- auth: none
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "ReadingSync": [
- {
- "ContentId": "book-uuid",
- "PercentRead": 45.6,
- "EntitlementId": "entitlement-id",
- "RemainingTimeMinutes": 120,
- "FirstReadTime": "2026-01-25T10:00:00Z",
- "LastModified": "2026-01-30T20:00:00Z"
- }
- ],
- "BookmarkSync": []
- }
-}
-
-docs {
- ## Sync Reading Progress - URL Path Token
-
- Synchronizes reading progress from Kobo device using token in URL path.
-
- **Method:** POST
-
- **Endpoint:** /sync/kobo/{kobo_device_token}/markup
-
- **Authentication:** URL path parameter (Kobo devices never send Bearer tokens)
-
- **Path Parameters:**
- - `kobo_device_token` (string): Device auth token from devices.auth_token
-
- **Request Body:**
- - `ReadingSync` (array): Reading progress data
- - `BookmarkSync` (array): Bookmarks and annotations
-
- **Response:** Success message
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Device sync disabled
- - 500: Internal server error
-
- **Important:** Kobo firmware requires token in URL path, cannot use Bearer header
-
- **Use Case:** Kobo devices syncing reading progress via stock firmware
-}
diff --git a/bruno/sync-koreader/api.bru b/bruno/sync-koreader/api.bru
deleted file mode 100644
index 306dd8b..0000000
--- a/bruno/sync-koreader/api.bru
+++ /dev/null
@@ -1,276 +0,0 @@
-meta {
- name: "Bookhoard KOReader Sync API"
- type: "collection"
- environment: {
- development: {
- base_url: "http://localhost:8765/api"
- },
- production: {
- base_url: "https://your-domain.com/api"
- }
- }
-}
-
-# Authentication Headers
-@name("KOReader Device Token")
-@AuthorizationHeader({
- name: "Authorization",
- value: "Bearer {{koreader_device_token}}"
-})
-{
- "token": "{{koreader_device_token}}"
-}
-
-# Sync Progress
-@name("Sync Progress - Single Book")
-POST {{environment.base_url}}/sync/koreader/progress
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "library_id": "optional-library-uuid",
- "books": [
- {
- "uuid": "book-uuid-here",
- "title": "Book Title",
- "authors": ["Author Name"],
- "progress": 0.45,
- "percentage": 0.45,
- "last_read": "2026-01-30T20:00:00Z",
- "chapter": 3,
- "epubcfi": "epubcfi(/6/4/2:15)",
- "character": 15432
- }
- ]
-}
-
-@name("Sync Progress - Multiple Books")
-POST {{environment.base_url}}/sync/koreader/progress
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "library_id": "optional-library-uuid",
- "books": [
- {
- "uuid": "book-1-uuid",
- "title": "Book One",
- "authors": ["Author One"],
- "progress": 0.25,
- "percentage": 0.25,
- "last_read": "2026-01-30T19:00:00Z",
- "chapter": 1,
- "epubcfi": "epubcfi(/6/4/2:10)"
- },
- {
- "uuid": "book-2-uuid",
- "title": "Book Two",
- "authors": ["Author Two"],
- "progress": 0.75,
- "percentage": 0.75,
- "last_read": "2026-01-30T20:00:00Z",
- "chapter": 8,
- "epubcfi": "epubcfi(/6/4/2:50)"
- }
- ]
-}
-
-@name("Sync Progress - With Bookmarks")
-POST {{environment.base_url}}/sync/koreader/progress
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "library_id": "optional-library-uuid",
- "books": [
- {
- "uuid": "book-uuid-here",
- "title": "Book Title",
- "authors": ["Author Name"],
- "progress": 0.45,
- "percentage": 0.45,
- "last_read": "2026-01-30T20:00:00Z",
- "bookmarks": [
- {
- "chapter": 3,
- "datetime": "2026-01-30T19:55:00Z",
- "notes": "highlighted text",
- "pos0": "epubcfi(/6/4/2:15)",
- "pos1": "epubcfi(/6/4/2:20)",
- "page": 45,
- "text": "highlighted text excerpt",
- "type": "highlight"
- }
- ]
- }
- ]
-}
-
-@name("Sync Progress - With Highlights and Notes")
-POST {{environment.base_url}}/sync/koreader/progress
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "library_id": "optional-library-uuid",
- "books": [
- {
- "uuid": "book-uuid-here",
- "title": "Book Title",
- "authors": ["Author Name"],
- "progress": 0.60,
- "percentage": 0.60,
- "last_read": "2026-01-30T20:00:00Z",
- "highlights": [
- {
- "datetime": "2026-01-30T19:50:00Z",
- "text": "Important passage",
- "chapter": 4,
- "pos0": "epubcfi(/6/4/2:20)",
- "pos1": "epubcfi(/6/4/2:30)",
- "page_start": 78,
- "page_end": 79
- }
- ],
- "notes": [
- {
- "datetime": "2026-01-30T19:52:00Z",
- "text": "My note about this chapter",
- "chapter": 4
- }
- ]
- }
- ]
-}
-
-# Get Metadata
-@name("Get Book Metadata")
-GET {{environment.base_url}}/sync/koreader/metadata/{{book_uuid}}
-Authorization: Bearer {{koreader_device_token}}
-
-@name("Get Metadata for Multiple Books")
-GET {{environment.base_url}}/sync/koreader/metadata
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "books": ["book-uuid-1", "book-uuid-2", "book-uuid-3"]
-}
-
-# Sync Bookmarks
-@name("Sync Bookmarks")
-POST {{environment.base_url}}/sync/koreader/bookmarks
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "library_id": "optional-library-uuid",
- "books": [
- {
- "uuid": "book-uuid",
- "bookmarks": [
- {
- "chapter": 3,
- "datetime": "2026-01-30T19:55:00Z",
- "notes": "Marked this chapter as important",
- "pos0": "epubcfi(/6/4/2:15)",
- "pos1": "epubcfi(/6/4/2:20)",
- "page": 45,
- "text": "Important passage",
- "type": "bookmark"
- }
- ]
- }
- ]
-}
-
-@name("Sync Highlights")
-POST {{environment.base_url}}/sync/koreader/highlights
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "library_id": "optional-library-uuid",
- "books": [
- {
- "uuid": "book-uuid",
- "highlights": [
- {
- "datetime": "2026-01-30T19:50:00Z",
- "text": "Important quote from book",
- "chapter": 4,
- "pos0": "epubcfi(/6/4/2:20)",
- "pos1": "epubcfi(/6/4/2:30)",
- "page_start": 78,
- "page_end": 79
- }
- ]
- }
- ]
-}
-
-@name("Sync Notes")
-POST {{environment.base_url}}/sync/koreader/notes
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "library_id": "optional-library-uuid",
- "books": [
- {
- "uuid": "book-uuid",
- "notes": [
- {
- "datetime": "2026-01-30T19:52:00Z",
- "text": "My personal note about this chapter",
- "chapter": 4
- }
- ]
- }
- ]
-}
-
-# Get Library
-@name("Get User Library for KOReader")
-GET {{environment.base_url}}/sync/koreader/library
-Authorization: Bearer {{koreader_device_token}}
-
-@name("Get Library for Specific Book")
-GET {{environment.base_url}}/sync/koreader/library
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "books": ["book-uuid-1", "book-uuid-2"]
-}
-
-# Immediate Sync Mode
-@name("Immediate Sync - Page Turn")
-POST {{environment.base_url}}/sync/koreader/progress
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "sync_mode": "immediate",
- "books": [
- {
- "uuid": "book-uuid",
- "percentage": 0.45678,
- "chapter": 3,
- "timestamp": "2026-01-30T20:00:00Z"
- }
- ]
-}
-
-# Checkpoint Sync Mode
-@name("Checkpoint Sync")
-POST {{environment.base_url}}/sync/koreader/progress
-Authorization: Bearer {{koreader_device_token}}
-Content-Type: application/json
-{
- "sync_mode": "checkpoint",
- "checkpoint_id": "checkpoint-uuid",
- "since_timestamp": "2026-01-30T19:00:00Z",
- "books": [
- {
- "uuid": "book-uuid-1",
- "percentage": 0.45,
- "chapter": 3
- },
- {
- "uuid": "book-uuid-2",
- "percentage": 0.75,
- "chapter": 8
- }
- ]
-}
diff --git a/bruno/system/get-scan-settings.bru b/bruno/system/get-scan-settings.bru
deleted file mode 100644
index 2373230..0000000
--- a/bruno/system/get-scan-settings.bru
+++ /dev/null
@@ -1,30 +0,0 @@
-meta {
- name: Get System Scan Settings
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/libraries/scan-settings
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-script:post-response {
- res.status.should.equal(200);
- res.body.type.should.equal("application/json");
- res.body.data.should.have.property('scan_frequency_minutes');
- res.body.data.should.have.property('auto_scan_enabled');
-}
-
-docs {
- ## Get System Scan Settings
-
- Retrieves current system-wide scan settings for all libraries.
-
- **Authentication**: Admin token required
- **Response**: Current scan frequency and auto-scan status
-}
diff --git a/bruno/system/update-scan-settings.bru b/bruno/system/update-scan-settings.bru
deleted file mode 100644
index 738f0c8..0000000
--- a/bruno/system/update-scan-settings.bru
+++ /dev/null
@@ -1,36 +0,0 @@
-meta {
- name: Update System Scan Settings
- type: http
- seq: 2
-}
-
-put {
- url: {{base_url}}/api/libraries/scan-settings
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- "scan_frequency_minutes": 30,
- "auto_scan_enabled": true
-}
-
-script:post-response {
- res.status.should.equal(200);
- res.body.type.should.equal("application/json");
- res.body.should.have.property('message');
-}
-
-docs {
- ## Update System Scan Settings
-
- Updates system-wide scan settings that apply to all libraries.
-
- **Authentication**: Admin token required
- **Request**: Scan frequency (15-1440 minutes) and enabled status
- **Response**: Success message
-}
diff --git a/bruno/universal-progress/Get Progress History.bru b/bruno/universal-progress/Get Progress History.bru
deleted file mode 100644
index cec5494..0000000
--- a/bruno/universal-progress/Get Progress History.bru
+++ /dev/null
@@ -1,68 +0,0 @@
-meta {
- name: Get Progress History
- type: http
- seq: 3
-}
-
-get {
- url: {{base_url}}/api/progress/{{mediaItemId}}/history
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{jwt}}
- Content-Type: application/json
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests['Has sessions key'] = body.sessions !== undefined;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Progress History
-
- Retrieves historical reading progress sessions for a specific media item.
-
- **Method:** GET
-
- **Endpoint:** /api/progress/{mediaItemId}/history
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `mediaItemId` (string): Media item UUID
-
- **Query Parameters:**
- - `limit` (number, optional): Maximum number of sessions to return
- - `offset` (number, optional): Offset for pagination
-
- **Response:**
- - `sessions` (array): Array of reading sessions
- - `session_id` (string): Session UUID
- - `start_time` (string): Session start timestamp
- - `end_time` (string): Session end timestamp
- - `start_percentage` (number): Progress at start
- - `end_percentage` (number): Progress at end
- - `device_id` (string): Device used
- - `duration_seconds` (number): Session duration
- - `total_sessions` (number): Total number of sessions
- - `total_reading_time` (number): Total reading time in seconds
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Media item not found
- - 500: Internal server error
-}
diff --git a/bruno/universal-progress/Get Universal Progress.bru b/bruno/universal-progress/Get Universal Progress.bru
deleted file mode 100644
index 0ec0740..0000000
--- a/bruno/universal-progress/Get Universal Progress.bru
+++ /dev/null
@@ -1,55 +0,0 @@
-meta {
- name: Get Universal Progress
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/progress/{{mediaItemId}}
- body: none
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{jwt}}
- Content-Type: application/json
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get Universal Progress
-
- Retrieves universal reading progress for a specific media item across all devices.
-
- **Method:** GET
-
- **Endpoint:** /api/progress/{mediaItemId}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `mediaItemId` (string): Media item UUID
-
- **Response:**
- - `media_item_id` (string): Media item UUID
- - `progress` (object): Universal progress data
- - `percentage` (number): Overall progress percentage
- - `epubcfi` (string): EPUB location
- - `page` (number): Current page
- - `chapter` (number): Current chapter
- - `devices` (array): Per-device progress data
- - `device_id` (string): Device UUID
- - `device_name` (string): Device name
- - `progress` (object): Device-specific progress
- - `last_updated` (string): Last update timestamp
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 404: Media item not found
- - 500: Internal server error
-}
diff --git a/bruno/universal-progress/Update Universal Progress.bru b/bruno/universal-progress/Update Universal Progress.bru
deleted file mode 100644
index c917420..0000000
--- a/bruno/universal-progress/Update Universal Progress.bru
+++ /dev/null
@@ -1,86 +0,0 @@
-meta {
- name: Update Universal Progress
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/progress/{{mediaItemId}}
- body: json
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{jwt}}
- Content-Type: application/json
-}
-
-body:json {
- {
- "source": "web",
- "location": {
- "percentage": 0.45,
- "page": 90,
- "total_pages": 200
- },
- "device_metadata": {
- "device_type": "web",
- "user_agent": "integration-test"
- }
- }
-}
-
-script:post-response {
- function onResponse(res) {
- if (res.getStatus() === 200) {
- const body = res.getBody();
- tests['Sync status success'] = body.sync_status === "success";
- tests['Progress updated'] = body.progress_updated === true;
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Universal Progress
-
- Updates universal reading progress for a media item from a specific device or source.
-
- **Method:** POST
-
- **Endpoint:** /api/progress/{mediaItemId}
-
- **Authentication:** Bearer token
-
- **Path Parameters:**
- - `mediaItemId` (string): Media item UUID
-
- **Request Body:**
- - `source` (string): Source identifier (web, kobo, koreader, device_id)
- - `location` (object): Progress location data
- - `percentage` (number): Progress percentage (0-1)
- - `page` (number, optional): Current page
- - `total_pages` (number, optional): Total pages
- - `epubcfi` (string, optional): EPUB location
- - `chapter` (number, optional): Current chapter
- - `device_metadata` (object): Device metadata
- - `device_type` (string): Device type
- - `user_agent` (string, optional): User agent string
-
- **Response:**
- - `sync_status` (string): Sync status (success, partial)
- - `progress_updated` (boolean): Whether progress was updated
- - `conflicts_detected` (array, optional): Any conflicts detected
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request data
- - 401: Unauthorized
- - 404: Media item not found
- - 500: Internal server error
-}
diff --git a/bruno/user/admin/Delete Account.bru b/bruno/user/admin/Delete Account.bru
deleted file mode 100644
index 03413f6..0000000
--- a/bruno/user/admin/Delete Account.bru
+++ /dev/null
@@ -1,53 +0,0 @@
-meta {
- name: Delete Account
- type: http
- seq: 4
-}
-
-delete {
- url: {{base_url}}/api/auth/account
- body: none
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete Account
-
- Permanently deletes user account and all associated data.
-
- **Method:** DELETE
-
- **Endpoint:** /api/auth/account
-
- **Authentication:** Required
-
- **Usage:**
- - **Self-deletion**: DELETE /api/auth/account (no parameters)
- - **Admin deletion**: DELETE /api/auth/account?user_id={uuid} (admin only)
-
- **Query Parameters (Admin only):**
- - `user_id` (string): UUID of user account to delete
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success
- - 400: Bad Request (invalid user_id or attempting to delete last admin)
- - 401: Unauthorized
- - 403: Forbidden (admin access required for user_id parameter)
- - 404: Not Found (user does not exist)
-
- **Protection Rules:**
- - Regular users can only delete their own account
- - Admins can delete any account including other users
- - Cannot delete the last admin account in the system
- - Admin role required to use user_id parameter
-
- **Warning:** This action cannot be undone and will permanently delete all user data including media items, ratings, and progress.
-}
\ No newline at end of file
diff --git a/bruno/user/admin/Delete User Account (Admin).bru b/bruno/user/admin/Delete User Account (Admin).bru
deleted file mode 100644
index 20d7d0f..0000000
--- a/bruno/user/admin/Delete User Account (Admin).bru
+++ /dev/null
@@ -1,54 +0,0 @@
-meta {
- name: Delete User Account (Admin)
- type: http
- seq: 6
-}
-
-delete {
- url: {{base_url}}/api/auth/account?user_id={{user_id}}
- body: none
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Delete User Account (Admin)
-
- Allows administrators to delete any user account by specifying user_id parameter.
-
- **Method:** DELETE
-
- **Endpoint:** /api/auth/account?user_id={user_id}
-
- **Authentication:** Required (Admin only)
-
- **Query Parameters:**
- - `user_id` (string, required for admin): UUID of the user account to delete
-
- **Usage Examples:**
- - **Self-deletion**: DELETE /api/auth/account (no user_id parameter)
- - **Admin deletion**: DELETE /api/auth/account?user_id=550e8400-e29b-41d4-a716-446655440000
-
- **Response:**
- - `message` (string): Success message indicating which account was deleted
-
- **Status Codes:**
- - 200: Success
- - 400: Bad Request (invalid user_id or attempting to delete last admin)
- - 401: Unauthorized
- - 403: Forbidden (admin access required for user_id parameter)
- - 404: Not Found (user does not exist)
-
- **Admin Protection Rules:**
- - Regular users can only delete their own account (no user_id parameter allowed)
- - Admins can delete any account including their own
- - Cannot delete the last admin account in the system
- - Admin role required to use user_id parameter
-
- **Variables:**
- - `user_id`: Set this to the UUID of the user you want to delete
-}
\ No newline at end of file
diff --git a/bruno/user/admin/List Users.bru b/bruno/user/admin/List Users.bru
deleted file mode 100644
index 28c2460..0000000
--- a/bruno/user/admin/List Users.bru
+++ /dev/null
@@ -1,99 +0,0 @@
-meta {
- name: List Users
- type: http
- seq: 5
-}
-
-get {
- url: {{base_url}}/api/auth/users
- body: none
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## List Users
-
- Retrieves a list of all users with complete user information.
-
- **Method:** GET
-
- **Endpoint:** /api/auth/users
-
- **Authentication:** Required (Admin only)
-
- **Response:** Array of user objects with complete information:
- - `id` (string): User ID (UUID)
- - `email` (string): Email address
- - `username` (string): Username
- - `first_name` (string): First name (empty if not set)
- - `last_name` (string): Last name (empty if not set)
- - `role` (string): User role ("user" or "admin")
- - `theme` (string): Theme preference (empty if default)
- - `max_devices` (integer): Maximum number of devices allowed
- - `device_count` (integer): Current number of registered devices
- - `created_at` (string): Creation timestamp (ISO 8601)
- - `updated_at` (string): Last update timestamp (ISO 8601)
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
-
- **Features:**
- - Admin-only endpoint with complete user information
- - Returns first_name, last_name, role, theme fields
- - Includes device limits and current device count
- - Useful for user management interfaces
-}
-
-get {
- url: {{base_url}}/api/auth/users
- body: none
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## List Users (Admin)
-
- Retrieves a list of all users with complete user information.
-
- **Method:** GET
-
- **Endpoint:** /api/auth/users
-
- **Authentication:** Required (Admin only)
-
- **Response:** Array of user objects with complete information:
- - `id` (string): User ID (UUID)
- - `email` (string): Email address
- - `username` (string): Username
- - `first_name` (string): First name (empty if not set)
- - `last_name` (string): Last name (empty if not set)
- - `role` (string): User role ("user" or "admin")
- - `theme` (string): Theme preference (empty if default)
- - `max_devices` (integer): Maximum number of devices allowed
- - `device_count` (integer): Current number of registered devices
- - `created_at` (string): Creation timestamp (ISO 8601)
- - `updated_at` (string): Last update timestamp (ISO 8601)
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
-
- **Enhanced Features:**
- - Now admin-only endpoint (moved from public to protected admin group)
- - Returns complete user profile information including names and role
- - Includes device limits and current device count for monitoring
- - Useful for comprehensive admin user management
-}
diff --git a/bruno/user/admin/Register Admin User.bru b/bruno/user/admin/Register Admin User.bru
deleted file mode 100644
index cea69ba..0000000
--- a/bruno/user/admin/Register Admin User.bru
+++ /dev/null
@@ -1,87 +0,0 @@
-meta {
- name: Register Admin User
- type: http
- seq: 4
-}
-
-post {
- url: {{base_url}}/api/auth/register
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "email": "maxdevices@example.com",
- "username": "maxdevicesuser",
- "password": "Test@Pass123!",
- "first_name": "Test",
- "last_name": "User",
- "role": "admin"
- }
-}
-
-script:post-response {
- function onResponse(res) {
- let data = res.getBody();
- return bru.setEnvVar("token", data.token, { persist: true });
- }
- onResponse(res);
-
-}
-
-// Test admin user aligns with Go integration tests
-// Email: maxdevices@example.com used in device cap tests
-// See TEST_DATA.md for shared test data documentation
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Register Admin User
-
- Creates a new admin user account with role-based restrictions.
-
- **Method:** POST
-
- **Endpoint:** /api/auth/register
-
- **Request Body:**
- - `email` (string): Email address
- - `username` (string): Username
- - `password` (string): Password
- - `first_name` (string, optional): First name
- - `last_name` (string, optional): Last name
- - `role` (string): Must be "admin"
-
- **Response:**
- - `token` (string): JWT token with admin role
- - `user` (object): User details
- - `id` (string): User ID
- - `email` (string): Email
- - `username` (string): Username
- - `theme` (string): User theme preference
- - `first_name` (string, optional): First name
- - `last_name` (string, optional): Last name
- - `role` (string): User role ("admin")
-
- **Status Codes:**
- - 201: Created
- - 400: Invalid input data
- - 403: Forbidden - admin creation restrictions apply
- - 409: User exists
-
- **Role Restrictions:**
- - **First User**: Anyone can create first admin (auto-assigned)
- - **Existing Admins Present**: Only authenticated admins can create new admin accounts
- - **Unauthenticated Users**: Cannot create admin accounts if any admin exists
- - **Security**: Requires admin authentication for subsequent admin creation
-
- **Usage Notes:**
- - Use this request only when specifically creating admin accounts
- - For regular user creation, use "Register User" request
- - Admin token will have elevated privileges for administrative operations
-
-}
diff --git a/bruno/user/admin/Update User Max Devices - Invalid Too High.bru b/bruno/user/admin/Update User Max Devices - Invalid Too High.bru
deleted file mode 100644
index 0fd9963..0000000
--- a/bruno/user/admin/Update User Max Devices - Invalid Too High.bru
+++ /dev/null
@@ -1,32 +0,0 @@
-meta {
- name: Update User Max Devices - Exceeds Maximum
- type: http
- seq: 3
-}
-
-put {
- url: {{base_url}}/api/auth/users/{{user_id}}/max-devices
- body: {
- max_devices: 101
- }
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-vars {
- user_id: 123e4567-e89b-12d3-a456-426614174000
- max_devices: 101
-}
-
-docs {
- ## Update User Max Devices - Invalid (Exceeds Maximum)
-
- Attempts to set max_devices to 101 (above maximum of 100).
-
- **Expected:** 400 Bad Request
- **Response:** `{"error": "validation error"}`
-}
diff --git a/bruno/user/admin/Update User Max Devices - Invalid Zero.bru b/bruno/user/admin/Update User Max Devices - Invalid Zero.bru
deleted file mode 100644
index 5b0f2e3..0000000
--- a/bruno/user/admin/Update User Max Devices - Invalid Zero.bru
+++ /dev/null
@@ -1,32 +0,0 @@
-meta {
- name: Update User Max Devices - Invalid Max Devices
- type: http
- seq: 2
-}
-
-put {
- url: {{base_url}}/api/auth/users/{{user_id}}/max-devices
- body: {
- max_devices: 0
- }
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-vars {
- user_id: 123e4567-e89b-12d3-a456-426614174000
- max_devices: 0
-}
-
-docs {
- ## Update User Max Devices - Invalid (Zero)
-
- Attempts to set max_devices to 0 (below minimum).
-
- **Expected:** 400 Bad Request
- **Response:** `{"error": "validation error"}`
-}
diff --git a/bruno/user/admin/Update User Max Devices - Missing ID.bru b/bruno/user/admin/Update User Max Devices - Missing ID.bru
deleted file mode 100644
index 9420c68..0000000
--- a/bruno/user/admin/Update User Max Devices - Missing ID.bru
+++ /dev/null
@@ -1,27 +0,0 @@
-meta {
- name: Update User Max Devices - Missing ID
- type: http
- seq: 4
-}
-
-put {
- url: {{base_url}}/api/auth/users//max-devices
- body: {
- max_devices: 10
- }
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update User Max Devices - Missing User ID
-
- Attempts to update max devices without providing user ID.
-
- **Expected:** 400 Bad Request
- **Response:** `{"error": "user id required"}`
-}
diff --git a/bruno/user/admin/Update User Max Devices - Success.bru b/bruno/user/admin/Update User Max Devices - Success.bru
deleted file mode 100644
index 32adf34..0000000
--- a/bruno/user/admin/Update User Max Devices - Success.bru
+++ /dev/null
@@ -1,32 +0,0 @@
-meta {
- name: Update User Max Devices - Success
- type: http
- seq: 1
-}
-
-put {
- url: {{base_url}}/api/auth/users/{{user_id}}/max-devices
- body: {
- max_devices: 5
- }
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-vars {
- user_id: 123e4567-e89b-12d3-a456-426614174000
- max_devices: 5
-}
-
-docs {
- ## Update User Max Devices - Success Case
-
- Successfully updates a user's max devices limit to 5.
-
- **Expected:** 200 OK
- **Response:** `{"message": "max devices updated"}`
-}
diff --git a/bruno/user/admin/Update User Max Devices.bru b/bruno/user/admin/Update User Max Devices.bru
deleted file mode 100644
index 5b7b0a7..0000000
--- a/bruno/user/admin/Update User Max Devices.bru
+++ /dev/null
@@ -1,95 +0,0 @@
-meta {
- name: Update User Max Devices
- type: http
- seq: 6
-}
-
-put {
- url: {{base_url}}/api/auth/users/{{user_id}}/max-devices
- body: {
- max_devices: {{max_devices}}
- }
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update User Max Devices (Admin)
-
- Updates the maximum number of devices a user can register.
-
- **Method:** PUT
-
- **Endpoint:** /api/auth/users/:id/max-devices
-
- **Authentication:** Required (Admin only)
-
- **URL Parameters:**
- - `id` (string): User ID (UUID)
-
- **Request Body:**
- ```json
- {
- "max_devices": 10
- }
- ```
- - `max_devices` (integer, required): Maximum devices (1-100)
-
- **Response:**
- ```json
- {
- "message": "max devices updated"
- }
- ```
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request (missing id, invalid max_devices, out of range)
- - 401: Unauthorized
- - 403: Forbidden (admin access required)
- - 500: Internal server error
-
- **Validation:**
- - `max_devices` must be between 1 and 100
- - User ID must be a valid UUID
- - User must exist
-
- **Features:**
- - Admin-only endpoint for managing user device quotas
- - Allows per-user device limits (default: 10)
- - Prevents excessive device registrations per user
- - Useful for multi-tenant or managed deployments
-
- **Examples:**
-
- Set max devices to 5:
- ```json
- {
- "max_devices": 5
- }
- ```
-
- Set max devices to 50 (premium user):
- ```json
- {
- "max_devices": 50
- }
- ```
-
- Set max devices to 1 (restricted user):
- ```json
- {
- "max_devices": 1
- }
- ```
-
- **Use Cases:**
- - Restrict free-tier users to 5 devices
- - Allow premium users up to 100 devices
- - Reduce device cap for suspicious accounts
- - Implement tiered device limits per user plan
-}
diff --git a/bruno/user/auth/Login User.bru b/bruno/user/auth/Login User.bru
deleted file mode 100644
index 7d6b3e2..0000000
--- a/bruno/user/auth/Login User.bru
+++ /dev/null
@@ -1,50 +0,0 @@
-meta {
- name: Login User
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/auth/login
- body: json
-}
-
-body:json {
- {
- "login": "testuser@example.com",
- "password": "Test@Pass123!"
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Login User
-
- Authenticates a user with email/username and password.
-
- **Method:** POST
-
- **Endpoint:** /api/auth/login
-
- **Request Body:**
- - `login` (string): Email or username
- - `password` (string): Password
-
- **Response:**
- - `token` (string): JWT token
- - `user` (object): User details
- - `id` (string): User ID
- - `email` (string): Email
- - `username` (string): Username
- - `theme` (string): User theme preference
- - `first_name` (string): First name
- - `last_name` (string): Last name
-
- **Status Codes:**
- - 200: Success
- - 401: Invalid credentials
-}
diff --git a/bruno/user/auth/Logout User.bru b/bruno/user/auth/Logout User.bru
deleted file mode 100644
index 7419b30..0000000
--- a/bruno/user/auth/Logout User.bru
+++ /dev/null
@@ -1,57 +0,0 @@
-meta {
- name: Logout User
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/auth/logout
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "refresh_token": "{{refresh_token}}"
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Logout User
-
- Logs out the user by revoking their refresh token. If no refresh token is provided, the request succeeds but no token is revoked.
-
- **Method:** POST
-
- **Endpoint:** /api/auth/logout
-
- **Authentication:** Bearer token (optional)
-
- **Request Body:**
- - `refresh_token` (string, optional): Refresh token to revoke
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success - user logged out (token revoked if provided)
- - 401: Unauthorized
-
- **Example Response:**
- ```json
- {
- "message": "logged out successfully"
- }
- ```
-
- **Note:** The access token will expire naturally after 1 hour. The refresh token is immediately revoked on logout, preventing future token refreshes.
-}
diff --git a/bruno/user/auth/Refresh Token.bru b/bruno/user/auth/Refresh Token.bru
deleted file mode 100644
index 5dcf0b7..0000000
--- a/bruno/user/auth/Refresh Token.bru
+++ /dev/null
@@ -1,68 +0,0 @@
-meta {
- name: Refresh Access Token
- type: http
- seq: 1
-}
-
-post {
- url: {{base_url}}/api/auth/refresh
- body: json
- auth: inherit
-}
-
-headers {
- Content-Type: application/json
-}
-
-body:json {
- {
- "refresh_token": "{{refresh_token}}"
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Refresh Access Token
-
- Refreshes an access token using a valid refresh token. Returns a new access token with 1-hour expiration.
-
- **Method:** POST
-
- **Endpoint:** /api/auth/refresh
-
- **Authentication:** Not required (refresh token is in request body)
-
- **Request Body:**
- - `refresh_token` (string): Valid refresh token UUID
-
- **Response:**
- - `access_token` (string): New JWT access token (1 hour expiration)
- - `token_type` (string): Token type (usually "Bearer")
- - `expires_in` (number): Token lifetime in seconds (3600)
-
- **Status Codes:**
- - 200: Success - new access token generated
- - 401: Unauthorized - invalid or expired refresh token
-
- **Example Response (Success):**
- ```json
- {
- "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
- "token_type": "Bearer",
- "expires_in": 3600
- }
- ```
-
- **Example Response (Invalid Token):**
- ```json
- {
- "error": "invalid or expired refresh token"
- }
- ```
-
- **Note:** Access tokens expire after 1 hour. Use the refresh token to obtain a new access token without requiring the user to log in again.
-}
diff --git a/bruno/user/auth/Register User.bru b/bruno/user/auth/Register User.bru
deleted file mode 100644
index b4ceca3..0000000
--- a/bruno/user/auth/Register User.bru
+++ /dev/null
@@ -1,90 +0,0 @@
-meta {
- name: Register User
- type: http
- seq: 2
-}
-
-post {
- url: {{base_url}}/api/auth/register
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "email": "testuser@example.com",
- "username": "testuser",
- "password": "Test@Pass123!",
- "first_name": "Test",
- "last_name": "User"
- }
-}
-
-// Test user aligns with Go integration tests
-// See TEST_DATA.md for shared test data documentation
-
-script:post-response {
- function onResponse(res) {
- let data = res.getBody();
- // If successful registration, set token environment variable
- if (res.getStatus() === 201 || res.getStatus() === 200) {
- if (data && data.access_token) {
-
- return bru.setEnvVar("token", data.access_token, { persist: true });
- }
- }
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Register User
-
- Creates a new user account with role-based restrictions.
-
- **Method:** POST
-
- **Endpoint:** /api/auth/register
-
- **Request Body:**
- - `email` (string): Email address
- - `username` (string): Username
- - `password` (string): Password
- - `first_name` (string, optional): First name
- - `last_name` (string, optional): Last name
- - `role` (string): User role ("user" or "admin")
-
- **Response:**
- - `token` (string): JWT token
- - `user` (object): User details
- - `id` (string): User ID
- - `email` (string): Email
- - `username` (string): Username
- - `theme` (string): User theme preference
- - `first_name` (string, optional): First name
- - `last_name` (string, optional): Last name
- - `role` (string): User role ("user" or "admin")
-
- **Status Codes:**
- - 201: Created
- - 400: Invalid input data
- - 403: Forbidden - role-based restrictions apply
- - 409: User exists
-
- **Role Restrictions:**
- - **First User**: Automatically gets admin role regardless of request
- - **Existing Admins Present**: Only authenticated admins can create new admin accounts
- - **No Admins Yet**: Anyone can create first admin (auto-assigned)
- - **Regular User Creation**: Anyone can create regular user accounts
- - **Unauthenticated Users**: Can only create first admin, not subsequent admins
-
- **Examples:**
- - First admin creation: `{"email": "admin@example.com", "username": "admin", "password": "password123", "role": "admin"}`
- - Regular user creation: `{"email": "user@example.com", "username": "user", "password": "password123", "role": "user"}`
-
-}
diff --git a/bruno/user/profile/Get Profile.bru b/bruno/user/profile/Get Profile.bru
deleted file mode 100644
index b52cb58..0000000
--- a/bruno/user/profile/Get Profile.bru
+++ /dev/null
@@ -1,40 +0,0 @@
-meta {
- name: Get Profile
- type: http
- seq: 1
-}
-
-get {
- url: {{base_url}}/api/auth/profile
- body: none
- auth: inherit
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Get User Profile
-
- Retrieves the authenticated user's profile.
-
- **Method:** GET
-
- **Endpoint:** /api/auth/profile
-
- **Authentication:** Required
-
- **Response:**
- - `id` (string): User ID
- - `email` (string): Email
- - `username` (string): Username
- - `theme` (string): User theme preference
- - `first_name` (string, optional): First name
- - `last_name` (string, optional): Last name
-
- **Status Codes:**
- - 200: Success
- - 401: Unauthorized
-}
diff --git a/bruno/user/profile/Update Email.bru b/bruno/user/profile/Update Email.bru
deleted file mode 100644
index bed83e9..0000000
--- a/bruno/user/profile/Update Email.bru
+++ /dev/null
@@ -1,46 +0,0 @@
-meta {
- name: Update Email
- type: http
- seq: 2
-}
-
-put {
- url: {{base_url}}/api/auth/email
- body: json
- auth: inherit
-}
-
-body {
- {
- "email": "newemail@example.com"
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Email
-
- Updates the authenticated user's email address.
-
- **Method:** PUT
-
- **Endpoint:** /api/auth/email
-
- **Authentication:** Required
-
- **Request Body:**
- - `email` (string, required): New email address (must be valid email format)
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid email format
- - 401: Unauthorized
- - 409: Email already taken
-}
\ No newline at end of file
diff --git a/bruno/user/profile/Update Password.bru b/bruno/user/profile/Update Password.bru
deleted file mode 100644
index 22d35a5..0000000
--- a/bruno/user/profile/Update Password.bru
+++ /dev/null
@@ -1,50 +0,0 @@
-meta {
- name: Update Password
- type: http
- seq: 3
-}
-
-put {
- url: {{base_url}}/api/auth/password
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "current_password": "password123",
- "new_password": "newpassword123",
- "confirm_password": "newpassword123"
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Password
-
- Updates the authenticated user's password.
-
- **Method:** PUT
-
- **Endpoint:** /api/auth/password
-
- **Authentication:** Required
-
- **Request Body:**
- - `current_password` (string, required): Current password for verification
- - `new_password` (string, required): New password (minimum 6 characters)
- - `confirm_password` (string, required): Confirmation of new password
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success
- - 400: Password validation failed
- - 401: Current password incorrect
- - 401: Unauthorized
-}
diff --git a/bruno/user/profile/Update Profile.bru b/bruno/user/profile/Update Profile.bru
deleted file mode 100644
index eefa219..0000000
--- a/bruno/user/profile/Update Profile.bru
+++ /dev/null
@@ -1,47 +0,0 @@
-meta {
- name: Update Profile
- type: http
- seq: 4
-}
-
-put {
- url: {{base_url}}/api/auth/profile
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "first_name": "Updated",
- "last_name": "Name"
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update User Profile
-
- Updates the authenticated user's profile information.
-
- **Method:** PUT
-
- **Endpoint:** /api/auth/profile
-
- **Authentication:** Required (Bearer token)
-
- **Request Body:**
- - `first_name` (string, optional): First name
- - `last_name` (string, optional): Last name
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid request
- - 401: Unauthorized
-}
diff --git a/bruno/user/profile/Update Theme.bru b/bruno/user/profile/Update Theme.bru
deleted file mode 100644
index 9efb1a1..0000000
--- a/bruno/user/profile/Update Theme.bru
+++ /dev/null
@@ -1,40 +0,0 @@
-meta {
- name: Update Theme
- type: http
- seq: 4
-}
-
-put {
- url: {{base_url}}/api/auth/theme
- body: json
- auth: inherit
-}
-
-body:json {
- {
- "theme": "dracula"
- }
-}
-
-docs {
- ## Update User Theme
-
- Updates the authenticated user's theme preference.
-
- **Method:** PUT
-
- **Endpoint:** /api/auth/theme
-
- **Authentication:** Required (Bearer token)
-
- **Request Body:**
- - `theme` (string): Theme name (tokyo-night, dracula, nord, solarized-dark, monokai, one-dark-pro, material-dark, catppuccin-mocha, catppuccin-macchiato, catppuccin-frappe, catppuccin-latte)
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid theme
- - 401: Unauthorized
-}
diff --git a/bruno/user/profile/Update Username.bru b/bruno/user/profile/Update Username.bru
deleted file mode 100644
index 2674e3d..0000000
--- a/bruno/user/profile/Update Username.bru
+++ /dev/null
@@ -1,46 +0,0 @@
-meta {
- name: Update Username
- type: http
- seq: 1
-}
-
-put {
- url: {{base_url}}/api/auth/username
- body: json
- auth: inherit
-}
-
-body {
- {
- "username": "newusername"
- }
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## Update Username
-
- Updates the authenticated user's username.
-
- **Method:** PUT
-
- **Endpoint:** /api/auth/username
-
- **Authentication:** Required
-
- **Request Body:**
- - `username` (string, required): New username (3-50 characters)
-
- **Response:**
- - `message` (string): Success message
-
- **Status Codes:**
- - 200: Success
- - 400: Invalid username
- - 401: Unauthorized
- - 409: Username already taken
-}
\ No newline at end of file
diff --git a/bruno/websocket_connect.bru b/bruno/websocket_connect.bru
deleted file mode 100644
index ab2b25f..0000000
--- a/bruno/websocket_connect.bru
+++ /dev/null
@@ -1,71 +0,0 @@
-meta {
- name: WebSocket - Connect and Receive Updates
- type: websocket
- seq: 1
-}
-
-vars {
- token: ""
-}
-
-websocket {
- url: {{base_url}}/ws/sync?token={{token}}
- body: {
- type: "ping",
- timestamp: "2026-01-30T20:00:00Z"
- }
- auth: inherit
-}
-
-headers {
- Authorization: Bearer {{token}}
-}
-
-script:post-response {
- function onResponse(res) {
- tests('Status is 101 (Switching Protocols)', res.getStatus() === 101);
- const headers = res.getHeaders();
- const connection = headers.get("Connection");
- const upgrade = headers.get("Upgrade");
- tests('Connection header has upgrade', connection && connection.toLowerCase().includes("upgrade"));
- tests('Upgrade header is websocket', upgrade && upgrade.toLowerCase().includes("websocket"));
- }
- onResponse(res);
-}
-
-settings {
- encodeUrl: true
- timeout: 0
-}
-
-docs {
- ## WebSocket - Connect and Receive Updates
-
- Establishes a WebSocket connection for real-time sync updates.
-
- **Type:** WebSocket
-
- **Endpoint:** /ws/sync?token={token}
-
- **Authentication:** Bearer token (via query parameter)
-
- **Query Parameters:**
- - `token` (string): JWT access token
-
- **Request Body:**
- - `type` (string): Message type (ping, subscribe, unsubscribe)
- - `timestamp` (string): ISO 8601 timestamp
-
- **Expected Response:**
- - Status: 101 Switching Protocols
- - Headers:
- - `Connection`: upgrade
- - `Upgrade`: websocket
-
- **Status Codes:**
- - 101: Switching Protocols - WebSocket established
- - 401: Unauthorized
- - 500: Internal server error
-
- **Note:** WebSocket connection allows real-time push notifications for sync updates, progress changes, and device activity.
-}