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
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
- Authentication
- Connection
- Message Format
- Message Types
- Client Implementation Guide
- Examples
- 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 identifiertimestamp(string, required): ISO 8601 timestampdata(object, required): Message payloadsource_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 datapercentage(number): 0.0 to 1.0current_page(number): Current page numbertotal_pages(number): Total pages in booklast_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 progresspercentage(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 bookannotation_type(string): Type of annotation (bookmark, highlight, note)data(object): Annotation-specific datapage(number): Page numbertext(string): Annotated text or notechapter(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 conflictnotification_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:
- Check JWT token is valid
- Verify server is running:
curl http://localhost:8765/health - Check firewall settings
- Verify WebSocket URL format
Connection Drops Frequently
Problem: WebSocket disconnects unexpectedly
Solutions:
- Check network stability
- Verify server keepalive settings
- Implement reconnection logic (see above)
- Check server logs for errors
No Messages Received
Problem: Connected but no messages
Solutions:
- Check onmessage handler is registered
- Verify initial_state message received
- Test with manual progress update via API
- Check browser console for errors
Authentication Errors
Problem: 401 Unauthorized
Solutions:
- Verify JWT token is not expired
- Check token has correct claims (user_id)
- For device tokens, verify Authorization header format
- Regenerate token via
/api/auth/login
High Memory Usage
Problem: Client memory increases over time
Solutions:
- Clean up old messages
- Don't store entire message history
- Use weak references for large data
- Implement message pagination
Performance Tips
-
Debounce UI Updates: Don't update DOM on every message
const debouncedUpdate = debounce(updateUI, 100); client.on("progress_update", (message) => { debouncedUpdate(message); }); -
Use Web Workers for heavy processing
-
Limit Message History: Keep only last N messages
-
Virtual Scrolling: For large lists of updates
-
Connection Pooling: Reuse connections across tabs
Security Best Practices
- Use WSS in Production: Always use secure WebSocket (wss://) in production
- Validate Tokens: Always verify JWT tokens on server
- Rate Limiting: Implement message rate limiting per connection
- Input Sanitization: Sanitize all message data before displaying
- 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)