Files
john-okeefe 4d321528b2 docs: update comprehensive API documentation and project guides
This commit updates all documentation files throughout the project:

- Updated IMPLEMENTATION_PLAN.md with new implementation details
- Updated PROJECT_GUIDELINES.md with coding standards and practices
- Updated README.md with current project information
- Updated SCREENSHOT_AUTOMATION.md with new automation details
- Added TEST_DATA.md with test fixtures data
- Updated cover_image_serving_plan.md with static URL patterns

Documentation API updates:
- Updated API reference documentation for all endpoints including:
  - Authentication (login, logout, register, refresh_token)
  - Book matching (auto_link, bulk_link, link_book, search)
  - Collections (CRUD operations, shelf mappings, auto-assign rules)
  - Conflicts (bulk operations, resolve/dismiss)
  - Devices (registration, approval, shelf management)
  - Highlights (create, update, delete, get)
  - Kobo sync (bookmark, markup, initialization, sync)
  - KOReader sync (library, metadata, bookmarks, progress)
  - Libraries (CRUD, folders, media items, stats)
  - Media items (bulk operations, CRUD)
  - Notes (CRUD operations)
  - OPDS (acquisition, feeds, publication)
  - Progress (reading progress tracking)
  - Queue (device queue management)
  - Ratings (star ratings)
  - Scanner (watch mode, scan operations)
  - Sync protocols (Kobo, KOReader)
  - Users (profile, password, admin operations)
  - WebSocket protocols

- Updated user guides (admin, dashboard, settings, sync)
- Updated device setup guides (Kobo, KOReader)
- Updated developer guides (testing, contributing, operations)
- Updated scripts/README.md
2026-02-27 17:06:22 -05:00

14 KiB

WebSocket API Documentation

Base URL: ws://localhost:8765/ws/sync Protocol: WebSocket (RFC 6455) Authentication: Required (JWT token or device token)


Table of Contents

  1. Authentication
  2. Connection
  3. Message Format
  4. Message Types
  5. Client Implementation Guide
  6. Examples
  7. Troubleshooting

Authentication

Option 1: JWT Token (Web/Mobile Clients)

Pass JWT token as query parameter:

ws://localhost:8765/ws/sync?token=<your_jwt_token>

How to get JWT token:

curl -X POST http://localhost:8765/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"password"}'

Response:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "user-uuid",
    "email": "user@example.com"
  }
}

Option 2: Device Token (Kobo, KOReader, etc.)

Pass device token in Authorization header:

ws://localhost:8765/ws/sync?token=any-value
Authorization: Bearer <device_token>

How to get device token: Device tokens are generated when devices are registered via the API.


Connection

Step 1: Connect to WebSocket

JavaScript Example:

const token = "your-jwt-token";
const ws = new WebSocket(`ws://localhost:8765/ws/sync?token=${token}`);

ws.onopen = () => {
  console.log("Connected to Bookhoard WebSocket");
};

ws.onerror = (error) => {
  console.error("WebSocket error:", error);
};

ws.onclose = (event) => {
  console.log("WebSocket closed:", event.code, event.reason);
};

Go Example:

import (
    "github.com/gorilla/websocket"
)

url := "ws://localhost:8765/ws/sync?token=" + token
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
    log.Fatal("Dial error:", err)
}
defer ws.Close()

Python Example:

import websocket

token = "your-jwt-token"
url = f"ws://localhost:8765/ws/sync?token={token}"

def on_message(ws, message):
    print(f"Received: {message}")

def on_error(ws, error):
    print(f"Error: {error}")

def on_close(ws, close_status_code, close_msg):
    print("Closed")

def on_open(ws):
    print("Connected")

ws = websocket.WebSocketApp(
    url,
    on_open=on_open,
    on_message=on_message,
    on_error=on_error,
    on_close=on_close
)

ws.run_forever()

Step 2: Handle Initial State

Immediately after connecting, you'll receive the initial state:

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);

  if (message.type === "initial_state") {
    console.log("Initial progress:", message.data.progress);
    console.log("Connected devices:", message.data.devices);

    // Store initial state
    initialState = message.data;
  }
};

Step 3: Handle Real-time Updates

Listen for subsequent updates:

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);

  switch (message.type) {
    case "progress_update":
      handleProgressUpdate(message);
      break;
    case "annotation_update":
      handleAnnotationUpdate(message);
      break;
    case "conflict":
      handleConflict(message);
      break;
    case "heartbeat":
      // Server ping, ignore
      break;
    default:
      console.log("Unknown message type:", message.type);
  }
};

function handleProgressUpdate(message) {
  const { book_id, percentage } = message.data;
  const source = message.source_device;

  console.log(`Progress update for ${book_id}: ${percentage * 100}%`);
  console.log(`Source: ${source.name} (${source.type})`);

  // Update UI
  updateProgressBar(book_id, percentage);
}

function handleAnnotationUpdate(message) {
  const { book_id, annotation_type, data } = message.data;
  const source = message.source_device;

  console.log(`Annotation update for ${book_id}: ${annotation_type}`);
  console.log(`Source: ${source.name}`);

  // Update UI
  showAnnotation(book_id, data);
}

function handleConflict(message) {
  const { book_id, notification_type, conflict_id } = message.data;

  console.log(`Conflict detected for ${book_id}: ${notification_type}`);

  // Show conflict resolution UI
  showConflictDialog(book_id, conflict_id);
}

Message Format

All messages follow this structure:

{
  "type": "message_type",
  "timestamp": "2026-02-01T12:00:00Z",
  "data": {
    // Message-specific data
  },
  "source_device": {
    "id": "device-uuid",
    "name": "Device Name",
    "type": "kobo|koreader|web|mobile"
  }
}

Fields:

  • type (string, required): Message type identifier
  • timestamp (string, required): ISO 8601 timestamp
  • data (object, required): Message payload
  • source_device (object, optional): Device that sent the update (not present for initial_state)

Message Types

1. initial_state

When: Immediately after connection

Purpose: Send current progress for all user's books

Data Structure:

{
  "type": "initial_state",
  "timestamp": "2026-02-01T12:00:00Z",
  "data": {
    "progress": {
      "<book_uuid>": {
        "percentage": 0.5,
        "current_page": 150,
        "total_pages": 300,
        "last_read": "2026-02-01T11:30:00Z"
      }
    },
    "devices": {
      "kobo": 2,
      "koreader": 1,
      "web": 3
    }
  }
}

Fields:

  • progress (object): Map of book UUID → progress data
    • percentage (number): 0.0 to 1.0
    • current_page (number): Current page number
    • total_pages (number): Total pages in book
    • last_read (string): ISO 8601 timestamp of last read
  • devices (object): Connection statistics by device type

2. progress_update

When: Any device syncs reading progress

Purpose: Notify all connected clients of progress change

Data Structure:

{
  "type": "progress_update",
  "timestamp": "2026-02-01T12:00:00Z",
  "data": {
    "book_id": "book-uuid-1",
    "percentage": 0.75
  },
  "source_device": {
    "id": "device-uuid-1",
    "name": "My Kobo Clara",
    "type": "kobo"
  }
}

Fields:

  • book_id (string): UUID of book with updated progress
  • percentage (number): New progress value (0.0 to 1.0)
  • source_device (object): Device that sent the update

Use Cases:

  • Update progress bar in real-time
  • Sync reading position across devices
  • Update "currently reading" lists

3. annotation_update

When: Any device syncs annotations, bookmarks, or highlights

Purpose: Share annotations across devices

Data Structure:

{
  "type": "annotation_update",
  "timestamp": "2026-02-01T12:00:00Z",
  "data": {
    "book_id": "book-uuid-1",
    "annotation_type": "bookmark",
    "data": {
      "page": 150,
      "text": "Great quote on page 150",
      "chapter": 5,
      "created_at": "2026-02-01T12:00:00Z"
    }
  },
  "source_device": {
    "id": "device-uuid-1",
    "name": "My Kobo Clara",
    "type": "kobo"
  }
}

Fields:

  • book_id (string): UUID of book
  • annotation_type (string): Type of annotation (bookmark, highlight, note)
  • data (object): Annotation-specific data
    • page (number): Page number
    • text (string): Annotated text or note
    • chapter (number): Chapter number (optional)
    • created_at (string): ISO 8601 timestamp
  • source_device (object): Device that created the annotation

Use Cases:

  • Show bookmarks on all devices
  • Share highlights between devices
  • Display reading notes

4. conflict

When: Sync conflict is detected

Purpose: Notify user of conflicting updates

Data Structure:

{
  "type": "conflict",
  "timestamp": "2026-02-01T12:00:00Z",
  "data": {
    "book_id": "book-uuid-1",
    "notification_type": "progress_conflict",
    "conflict_id": "conflict-uuid-1"
  }
}

Fields:

  • book_id (string): UUID of book with conflict
  • notification_type (string): Type of conflict (progress_conflict, annotation_conflict)
  • conflict_id (string): UUID of conflict record

Use Cases:

  • Prompt user to resolve conflict
  • Show conflict resolution UI
  • Log conflict for manual review

5. heartbeat

When: Server sends ping every 30 seconds

Purpose: Keep connection alive

Data Structure:

{
  "type": "heartbeat",
  "timestamp": "2026-02-01T12:00:00Z"
}

Handling: Clients should respond automatically (handled by WebSocket protocol)


Client Implementation Guide

Reconnection Strategy

Implement exponential backoff for reconnection:

let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
const baseReconnectDelay = 1000; // 1 second

function connect() {
  const token = getAuthToken();
  const ws = new WebSocket(`ws://localhost:8765/ws/sync?token=${token}`);

  ws.onopen = () => {
    console.log("Connected");
    reconnectAttempts = 0; // Reset on successful connection
  };

  ws.onclose = (event) => {
    if (reconnectAttempts < maxReconnectAttempts) {
      const delay = baseReconnectDelay * Math.pow(2, reconnectAttempts);
      console.log(`Reconnecting in ${delay}ms...`);

      setTimeout(() => {
        reconnectAttempts++;
        connect();
      }, delay);
    } else {
      console.error("Max reconnection attempts reached");
    }
  };

  ws.onmessage = handleIncomingMessage;

  return ws;
}

Error Handling

Handle common errors:

ws.onerror = (error) => {
  console.error("WebSocket error:", error);

  // Check specific error types
  if (error.code === 1006) {
    console.error("Abnormal closure - server may be down");
  } else if (error.code === 1008) {
    console.error("Policy violation - check authentication");
  }
};

Message Queueing

Queue messages while disconnected:

let messageQueue = [];
let isConnected = false;

function queueMessage(message) {
  if (isConnected) {
    ws.send(JSON.stringify(message));
  } else {
    messageQueue.push(message);
  }
}

ws.onopen = () => {
  isConnected = true;

  // Send queued messages
  while (messageQueue.length > 0) {
    const message = messageQueue.shift();
    ws.send(JSON.stringify(message));
  }
};

ws.onclose = () => {
  isConnected = false;
};

Examples

Complete JavaScript Client

class BookhoardWebSocket {
  constructor(token) {
    this.token = token;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 10;
    this.messageHandlers = {};
  }

  connect() {
    const url = `ws://localhost:8765/ws/sync?token=${this.token}`;
    this.ws = new WebSocket(url);

    this.ws.onopen = () => {
      console.log("Connected to Bookhoard");
      this.reconnectAttempts = 0;
    };

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };

    this.ws.onerror = (error) => {
      console.error("WebSocket error:", error);
    };

    this.ws.onclose = () => {
      this.reconnect();
    };
  }

  handleMessage(message) {
    const handler = this.messageHandlers[message.type];
    if (handler) {
      handler(message);
    }
  }

  on(messageType, callback) {
    this.messageHandlers[messageType] = callback;
  }

  reconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      const delay = 1000 * Math.pow(2, this.reconnectAttempts);
      console.log(`Reconnecting in ${delay}ms...`);

      setTimeout(() => {
        this.reconnectAttempts++;
        this.connect();
      }, delay);
    }
  }

  disconnect() {
    if (this.ws) {
      this.ws.close();
    }
  }
}

// Usage
const token = "your-jwt-token";
const client = new BookhoardWebSocket(token);

client.on("initial_state", (message) => {
  console.log("Initial state received:", message.data);
  updateUIWithInitialState(message.data);
});

client.on("progress_update", (message) => {
  console.log("Progress updated:", message.data);
  updateProgressBar(message.data.book_id, message.data.percentage);
});

client.on("annotation_update", (message) => {
  console.log("Annotation updated:", message.data);
  showNotification("New annotation from " + message.source_device.name);
});

client.on("conflict", (message) => {
  console.log("Conflict detected:", message.data);
  showConflictDialog(message.data);
});

client.connect();

Troubleshooting

Connection Fails

Problem: Can't connect to WebSocket

Solutions:

  1. Check JWT token is valid
  2. Verify server is running: curl http://localhost:8765/health
  3. Check firewall settings
  4. Verify WebSocket URL format

Connection Drops Frequently

Problem: WebSocket disconnects unexpectedly

Solutions:

  1. Check network stability
  2. Verify server keepalive settings
  3. Implement reconnection logic (see above)
  4. Check server logs for errors

No Messages Received

Problem: Connected but no messages

Solutions:

  1. Check onmessage handler is registered
  2. Verify initial_state message received
  3. Test with manual progress update via API
  4. Check browser console for errors

Authentication Errors

Problem: 401 Unauthorized

Solutions:

  1. Verify JWT token is not expired
  2. Check token has correct claims (user_id)
  3. For device tokens, verify Authorization header format
  4. Regenerate token via /api/auth/login

High Memory Usage

Problem: Client memory increases over time

Solutions:

  1. Clean up old messages
  2. Don't store entire message history
  3. Use weak references for large data
  4. Implement message pagination

Performance Tips

  1. Debounce UI Updates: Don't update DOM on every message

    const debouncedUpdate = debounce(updateUI, 100);
    client.on("progress_update", (message) => {
      debouncedUpdate(message);
    });
    
  2. Use Web Workers for heavy processing

  3. Limit Message History: Keep only last N messages

  4. Virtual Scrolling: For large lists of updates

  5. Connection Pooling: Reuse connections across tabs


Security Best Practices

  1. Use WSS in Production: Always use secure WebSocket (wss://) in production
  2. Validate Tokens: Always verify JWT tokens on server
  3. Rate Limiting: Implement message rate limiting per connection
  4. Input Sanitization: Sanitize all message data before displaying
  5. Origin Checks: Validate WebSocket origin on server

Support

For issues or questions:

  • GitHub Issues: [Bookhoard Repository]
  • Documentation: [Bookhoard Docs]
  • API Reference: [Bookhoard API Docs]

Last Updated: 2026-02-01 Version: 1.0.0 Protocol: WebSocket (RFC 6455)