docs: restructure documentation into audience-based portals

BREAKING CHANGE: Documentation URLs have changed

New structure:
- user/ - End-user documentation (device setup, sync guides, frontend)
- developer/ - Developer documentation (API reference, protocols, specs)
- operations/ - Operations documentation (deployment, troubleshooting)
- contributing/ - Contribution guides

Changes:
- Created portal INDEX.md files for each audience section
- Moved device guides to user/devices/ (kobo-setup.md, koreader-setup.md)
- Moved API docs to developer/ (api-reference.md, collections-api.md)
- Moved sync guide to user/sync-guide.md
- Moved troubleshooting to operations/troubleshooting.md
- Moved all split API docs to developer/api/
- Renamed protocol files (kobo-protocol.md, koreader-protocol.md)
- Added placeholder user guides (frontend, user-areas, settings, admin)
- Updated all internal links to new paths
- Updated Go code (http_handler.go, navigation.go) for new paths
- Updated main INDEX.md for audience-based navigation

Benefits:
- Clear separation of user and developer documentation
- Scalable structure for future user guide expansion
- Better organization and discoverability
- Audience-specific landing pages

Related to DOCS_IMPLEMENTATION_PLAN.md Phase 2 completion
This commit is contained in:
2026-02-02 15:58:34 -05:00
parent ece90c0f1f
commit 253f56399d
76 changed files with 372 additions and 207 deletions
+77
View File
@@ -0,0 +1,77 @@
# Developer Documentation
Welcome to the Bookhoard developer documentation. This section contains API references, protocol specifications, and technical documentation for integrating with Bookhoard.
## 📚 API Documentation
### Complete API Reference
- **[API Reference](api-reference.md)** - Complete REST API reference
- Authentication
- User management
- Libraries
- Media items
- Reading progress
- Notes & highlights
- Analytics
- Book matching
- OPDS
- Sync protocols (KOReader, Kobo)
- WebSocket
### Split Endpoint Documentation
Browse individual endpoint documentation with interactive API Explorer:
- **[API Documentation](api/INDEX.md)** - Split endpoint docs
- [Authentication](api/authentication/) - Register, login, tokens
- [Users](api/users/) - Profile management
- [Libraries](api/libraries/) - Library operations
- [Media Items](api/media-items/) - Book/ebook management
- [Progress](api/progress/) - Reading progress tracking
- [Notes](api/notes/) - User notes
- [Highlights](api/highlights/) - Book highlights
- [Ratings](api/ratings/) - Book ratings
- [Devices](api/devices/) - Device management
- [Analytics](api/analytics/) - Usage statistics
- [Book Matching](api/book-matching/) - Search and link books
- [Collections](api/collections/) - Collection management
- [OPDS](api/opds/) - Open Publication Distribution
- [Sync Protocols](api/sync/) - KOReader and Kobo sync
- [WebSocket](api/websocket/) - Real-time events
### Collections API
- **[Collections API](collections-api.md)** - Complete collections API reference
- Create and manage collections
- Auto-assign rules
- Test rules
- Bulk operations
- Device shelf mappings
## 🔌 Protocol Specifications
### Sync Protocols
- **[Kobo Sync Protocol](api/sync/kobo-protocol.md)** - Kobo device sync protocol
- Kobo markup sync
- Library fetch
- Authentication
- **[KOReader Sync Protocol](api/sync/koreader-protocol.md)** - KOReader sync protocol
- Progress sync
- Metadata fetch
- Conflict handling
### WebSocket Protocol
- **[WebSocket API](websocket-api.md)** - Real-time sync and events
- Connection flow
- Message format
- Event types
- Authentication
---
**Looking for user documentation?** See the [User Portal](../user/INDEX.md)
**Need to deploy?** See the [Operations Portal](../operations/INDEX.md)
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
# API Documentation
Complete reference for Bookhoard REST API endpoints.
## Quick Links
- [Authentication](authentication/) - User registration, login, tokens
- [Users](users/) - Profile management
- [Libraries](libraries/) - Library management
- [Media Items](media-items/) - Book/ebook operations
- [Progress](progress/) - Reading progress tracking
- [Notes](notes/) - User notes management
- [Highlights](highlights/) - Book highlights
- [Ratings](ratings/) - Book ratings
- [Devices](devices/) - Device registration and sync
- [Analytics](analytics/) - Usage statistics
- [Book Matching](book-matching/) - Search and link books
- [Collections](collections/) - See [Collections API](../collections-api.md)
- [OPDS](opds/) - Open Publication Distribution
- [Sync Protocols](sync/) - KOReader and Kobo sync
- [WebSocket](websocket/) - Real-time sync events
---
## Authentication
See [Authentication Endpoints](authentication/)
## Users & Profiles
See [User Management](users/)
## Libraries
See [Library Management](libraries/)
## Media Items
See [Media Item Operations](media-items/)
## Reading Progress
See [Progress Tracking](progress/)
## Notes & Highlights
See [Notes Management](notes/) and [Highlights Management](highlights/)
## Ratings
See [Ratings System](ratings/)
## Device Management
See [Device Registration & Sync](devices/)
## Analytics
See [Usage Analytics](analytics/)
## Book Matching & Linking
See [Book Matching](book-matching/)
## Collections
See [Collections API](../collections-api.md) or [Collections Endpoints](collections/)
## OPDS
See [OPDS Feeds](opds/)
## Sync Protocols
See [KOReader Sync](sync/) and [Kobo Sync](sync/)
## WebSocket
See [WebSocket Protocol](websocket/)
@@ -0,0 +1,49 @@
# Get Reading Statistics
Retrieve reading statistics for a date range.
**Endpoint**: `GET /api/analytics/reading-stats`
**Auth**: Required
## Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| start_date | string | No | Start date (ISO 8601 format) |
| end_date | string | No | End date (ISO 8601 format) |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/analytics/reading-stats?start_date=2026-01-01&end_date=2026-01-31
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"pages_read": 1250,
"books_completed": 5,
"reading_time_hours": 42.5,
"sessions_count": 28,
"average_session_minutes": 91
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid date format |
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,49 @@
# Login User
Authenticate with email and password.
**Endpoint**: `POST /api/auth/login`
**Auth**: Not required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| email | string | Yes | User's email address |
| password | string | Yes | User's password |
### Example Request
```json
{
"email": "user@example.com",
"password": "SecureP@ss123!"
}
```
## Response (200 OK)
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "d4f5g6h7...",
"user": {
"id": "uuid-here",
"email": "user@example.com",
"username": "john",
"role": "user"
}
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid email or password |
| 400 | Missing required fields |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,35 @@
# Logout User
Invalidate the current JWT token.
**Endpoint**: `POST /api/auth/logout`
**Auth**: Required
**Content-Type**: `application/json`
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token (e.g., `Bearer eyJhbG...`) |
### Example Request
```http
POST /api/auth/logout
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (204 No Content)
No response body.
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 403 | Token already invalidated |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,41 @@
# Refresh Token
Obtain a new JWT token using a refresh token.
**Endpoint**: `POST /api/auth/refresh`
**Auth**: Not required (uses refresh token)
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| refresh_token | string | Yes | Valid refresh token |
### Example Request
```json
{
"refresh_token": "d4f5g6h7..."
}
```
## Response (200 OK)
```json
{
"token": "new-jwt-token",
"refresh_token": "new-refresh-token"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired refresh token |
| 400 | Missing refresh token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,57 @@
# Register User
Create a new user account.
**Endpoint**: `POST /api/auth/register`
**Auth**: Not required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| email | string | Yes | User's email address |
| username | string | Yes | Desired username |
| password | string | Yes | Password (min 8 chars) |
| first_name | string | No | User's first name |
| last_name | string | No | User's last name |
### Example Request
```json
{
"email": "user@example.com",
"username": "john",
"password": "SecureP@ss123!",
"first_name": "John",
"last_name": "Doe"
}
```
## Response (201 Created)
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "d4f5g6h7...",
"user": {
"id": "uuid-here",
"email": "user@example.com",
"username": "john",
"role": "user",
"theme": "tokyo-night",
"created_at": "2026-01-31T10:00:00Z"
}
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid email format, weak password, or missing fields |
| 409 | Email or username already exists |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,92 @@
# Link Book
Link a device book to a Bookhoard media item. Supports bulk linking.
**Endpoint**: `POST /api/sync/bulk-link-books` or `POST /api/sync/auto-link-books`
**Auth**: Required
**Content-Type**: `application/json`
## Manual Link Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| links | array | Yes | List of book links |
| links[].unlinked_book_id | string | Yes | Device book UUID |
| links[].media_item_id | string | Yes | Bookhoard media item UUID |
| links[].confidence_score | float | No | Match confidence (0-1) |
### Example Manual Link Request
```json
{
"links": [
{
"unlinked_book_id": "uuid-1",
"media_item_id": "uuid-2",
"confidence_score": 1.0
}
]
}
```
## Auto-Link Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| confidence_threshold | float | No | Minimum confidence for auto-link (default: 0.8) |
| limit | integer | No | Maximum books to auto-link (default: 50) |
### Example Auto-Link Request
```json
{
"confidence_threshold": 0.8,
"limit": 50
}
```
## Response (200 OK) - Manual Link
```json
{
"results": [
{
"unlinked_book_id": "uuid-1",
"status": "success",
"media_item_id": "uuid-2"
}
],
"total": 1,
"successful": 1,
"failed": 0
}
```
## Response (200 OK) - Auto-Link
```json
{
"auto_linked": 15,
"results": [
{
"unlinked_book_id": "uuid-1",
"title": "The Hobbit",
"matched_media_item_id": "uuid-2",
"confidence": 0.95,
"match_method": "sha256_match"
}
]
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid link data |
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,56 @@
# Search Books for Matching
Query books to find potential matches for linking.
**Endpoint**: `POST /api/sync/books/query`
**Auth**: Required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| identifiers | array | No | List of identifiers (ISBN, UUID) |
| sha256 | string | No | SHA256 hash of book file |
| title | string | No | Book title |
| author | string | No | Book author |
| file_size | integer | No | File size in bytes |
### Example Request
```json
{
"identifiers": ["isbn:978-0345391802", "uuid:abc-123"],
"sha256": "a1b2c3d4e5f6abc123...",
"title": "The Hobbit",
"author": "J.R.R. Tolkien",
"file_size": 2456789
}
```
## Response (200 OK)
```json
{
"matches": [
{
"media_item_id": "uuid-123",
"bookhoard_uuid": "uuid-123",
"confidence": 1.0,
"match_method": "uuid_match"
}
],
"action": "auto_link"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid query parameters |
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+35
View File
@@ -0,0 +1,35 @@
# Collections API
See [Collections API](../../collections-api.md) for complete collections documentation.
## Quick Links
- [List Collections](list_collections.md) - Get all collections
- [Get Collection](get_collection.md) - Get collection details
- [Create Collection](create_collection.md) - Create a new collection
- [Update Collection](update_collection.md) - Update collection metadata
- [Delete Collection](delete_collection.md) - Delete a collection
- [Add Auto-Assign Rule](add_auto_assign_rule.md) - Add automatic book assignment rule
- [Remove Auto-Assign Rule](remove_auto_assign_rule.md) - Remove assignment rule
- [Test Rule](test_rule.md) - Test assignment rule
- [Bulk Assign](bulk_assign.md) - Bulk assign books to collection
- [Create Shelf Mapping](create_shelf_mapping.md) - Map collection to device shelf
- [Delete Shelf Mapping](delete_shelf_mapping.md) - Remove shelf mapping
---
## Overview
Collections allow you to organize your books into custom groups with automatic assignment rules.
## Features
- **Manual Assignment**: Add specific books to collections
- **Auto-Assignment**: Define rules to automatically assign books based on metadata
- **Device Sync**: Collections sync as shelves on Kobo/KOReader devices
- **Flexible Rules**: Filter by genre, series, tags, and more
## See Also
- [Collections API](../../collections-api.md) - Complete API reference
- [Sync Guide](../../user/sync-guide.md) - How collections sync to devices
@@ -0,0 +1,85 @@
# Add Auto-Assign Rule
Add an automatic book assignment rule to a collection.
**Endpoint**: `POST /api/collections/{id}/rules`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| id | string (UUID) | Yes | Collection UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
| value | string/number | Yes | Value to compare against |
| priority | integer | No | Rule priority (1 = highest, default: 1) |
| enabled | boolean | No | Whether rule is active (default: true) |
### Supported Fields
| Field | Type | Description |
|-------|------|-------------|
| genre | string | Book genre |
| author | string | Book author |
| series | string | Book series name |
| language | string | Book language |
| publisher | string | Publisher name |
| copyright_year | number | Publication year (numeric comparison) |
| tags | string | Book tags |
### Supported Operators
| Operator | Type | Description |
|----------|------|-------------|
| equals | all | Exact match |
| not_equals | all | Not equal |
| contains | string | Contains substring (case-insensitive) |
| not_contains | string | Does not contain |
| starts_with | string | Starts with (case-insensitive) |
| ends_with | string | Ends with (case-insensitive) |
| greater_than | number | Greater than |
| less_than | number | Less than |
### Example Request
```json
{
"field": "genre",
"operator": "equals",
"value": "Science Fiction",
"priority": 1,
"enabled": true
}
```
## Response (201 Created)
```json
{
"id": "rule-uuid-here",
"field": "genre",
"operator": "equals",
"value": "Science Fiction",
"priority": 1,
"enabled": true,
"created_at": "2026-02-01T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid request (validation failed) |
| 401 | Authentication required |
| 404 | Collection not found |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,52 @@
# Bulk Assign Books
Add multiple books to a collection at once.
**Endpoint**: `POST /api/collections/{id}/books`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| id | string (UUID) | Yes | Collection UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| book_ids | array of UUID | Yes | Array of book IDs to add |
### Example Request
```json
{
"book_ids": [
"660e8400-e29b-41d4-a716-446655440000",
"770e8400-e29b-41d4-a716-446655440000",
"880e8400-e29b-41d4-a716-446655440000"
]
}
```
## Response (204 No Content)
Books added to collection successfully. No response body.
## Notes
- Books already in the collection are ignored
- This is more efficient than adding books individually
- Maximum 1000 books per request
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid request (validation failed) |
| 401 | Authentication required |
| 404 | Collection or book(s) not found |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,96 @@
# Create Collection
Create a new collection.
**Endpoint**: `POST /api/collections`
**Auth**: Required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| name | string | Yes | Collection name (max 255 chars) |
| description | string | No | Collection description |
| color | string | No | Hex color code (e.g., "#FF5733") |
| icon | string | No | Emoji icon (e.g., "🚀", "📖") |
| auto_assign_rules | array | No | Array of rule objects |
| view_settings | object | No | Per-device display preferences |
### Auto-Assign Rule Object
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
| value | string/number | Yes | Value to compare against |
| priority | integer | No | Rule priority (1 = highest, default: 1) |
| enabled | boolean | No | Whether rule is active (default: true) |
### Example Request
```json
{
"name": "To Read",
"description": "Books I want to read soon",
"color": "#00FF00",
"icon": "📖",
"auto_assign_rules": [
{
"field": "tags",
"operator": "contains",
"value": "to-read",
"priority": 1,
"enabled": true
}
],
"view_settings": {
"kobo": {
"view_mode": "grid"
},
"koreader": {
"view_mode": "list"
}
}
}
```
## Response (201 Created)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "To Read",
"description": "Books I want to read soon",
"color": "#00FF00",
"icon": "📖",
"auto_assign_rules": [
{
"field": "tags",
"operator": "contains",
"value": "to-read",
"priority": 1,
"enabled": true
}
],
"view_settings": {
"kobo": {
"view_mode": "grid"
},
"koreader": {
"view_mode": "list"
}
},
"created_at": "2026-02-01T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid request (validation failed) |
| 401 | Authentication required |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,69 @@
# Create Shelf Mapping
Map a collection to a device shelf for syncing.
**Endpoint**: `POST /api/devices/{deviceId}/collections`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| deviceId | string (UUID) | Yes | Device UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| collection_id | string (UUID) | Yes | Collection UUID to map |
| device_shelf_name | string | Yes | Name of the shelf on the device |
| sync_direction | string | No | Sync direction (default: "bidirectional") |
### Sync Directions
| Direction | Description |
|-----------|-------------|
| bidirectional | Sync both ways between Bookhoard and device |
| book_to_hoard | Bookhoard → Device only |
| device_to_hoard | Device → Bookhoard only |
| none | No sync (mapping only for reference) |
### Example Request
```json
{
"collection_id": "550e8400-e29b-41d4-a716-446655440000",
"device_shelf_name": "Sci-Fi",
"sync_direction": "bidirectional"
}
```
## Response (201 Created)
```json
{
"id": "990e8400-e29b-41d4-a716-446655440000",
"collection_id": "550e8400-e29b-41d4-a716-446655440000",
"collection_name": "Science Fiction",
"device_shelf_name": "Sci-Fi",
"sync_direction": "bidirectional",
"created_at": "2026-02-01T10:00:00Z"
}
```
## Use Case
Collections can be synced to device-specific shelves (Kobo, KOReader). This allows automatic organization of books on your e-reader based on your Bookhoard collections.
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid request (validation failed) |
| 401 | Authentication required |
| 404 | Device or collection not found |
| 409 | Mapping already exists |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,27 @@
# Delete Collection
Delete a collection. Books are NOT deleted.
**Endpoint**: `DELETE /api/collections/{id}`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| id | string (UUID) | Yes | Collection UUID |
## Response (204 No Content)
Collection deleted successfully. No response body.
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Authentication required |
| 404 | Collection not found |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,34 @@
# Delete Shelf Mapping
Remove a collection-to-shelf mapping for a device.
**Endpoint**: `DELETE /api/devices/{deviceId}/collections/{collectionId}`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| deviceId | string (UUID) | Yes | Device UUID |
| collectionId | string (UUID) | Yes | Collection UUID |
## Response (204 No Content)
Shelf mapping deleted successfully. No response body.
## Notes
- Deleting a mapping does NOT delete the collection
- Deleting a mapping does NOT delete books from the device
- It only removes the association between the collection and device shelf
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Authentication required |
| 404 | Device or collection not found |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,54 @@
# Get Collection
Get single collection with all books.
**Endpoint**: `GET /api/collections/{id}`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| id | string (UUID) | Yes | Collection UUID |
## Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| include_books | boolean | No | Include books in response (default: true) |
| limit | integer | No | Number of books to return (default: 50) |
| offset | integer | No | Number of books to skip (default: 0) |
## Response (200 OK)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Science Fiction",
"description": "My sci-fi collection",
"color": "#FF5733",
"icon": "🚀",
"books": [
{
"media_item_id": "660e8400-e29b-41d4-a716-446655440000",
"title": "Foundation",
"author": "Isaac Asimov",
"cover_image_path": "/covers/foundation.jpg"
}
],
"auto_assign_rules": [],
"view_settings": {},
"created_at": "2026-02-01T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Authentication required |
| 404 | Collection not found |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,57 @@
# List Collections
Get all collections for the authenticated user.
**Endpoint**: `GET /api/collections`
**Auth**: Required
**Content-Type**: `application/json`
## Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| limit | integer | No | Number of collections to return (default: 50) |
| offset | integer | No | Number of collections to skip (default: 0) |
## Response (200 OK)
```json
{
"collections": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Science Fiction",
"description": "My sci-fi collection",
"color": "#FF5733",
"icon": "🚀",
"auto_assign_rules": [
{
"field": "genre",
"operator": "equals",
"value": "Science Fiction",
"priority": 1,
"enabled": true
}
],
"view_settings": {
"kobo": {
"view_mode": "grid",
"sort_order": "name",
"items_per_page": 24
}
},
"created_at": "2026-02-01T10:00:00Z"
}
],
"total": 1
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Authentication required |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,28 @@
# Remove Auto-Assign Rule
Remove an automatic book assignment rule from a collection.
**Endpoint**: `DELETE /api/collections/{collectionId}/rules/{ruleId}`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| collectionId | string (UUID) | Yes | Collection UUID |
| ruleId | string (UUID) | Yes | Rule UUID |
## Response (204 No Content)
Rule deleted successfully. No response body.
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Authentication required |
| 404 | Collection or rule not found |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,71 @@
# Test Rule
Test which books would match given rules without saving.
**Endpoint**: `POST /api/collections/test-rules`
**Auth**: Required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| rules | array | Yes | Array of rule objects to test |
### Rule Object
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| field | string | Yes | Field to match on (genre, author, series, language, publisher, copyright_year, tags) |
| operator | string | Yes | Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than) |
| value | string/number | Yes | Value to compare against |
### Example Request
```json
{
"rules": [
{
"field": "genre",
"operator": "equals",
"value": "Science Fiction"
},
{
"field": "author",
"operator": "contains",
"value": "Asimov"
}
]
}
```
## Response (200 OK)
```json
{
"matches": [
{
"media_item_id": "660e8400-e29b-41d4-a716-446655440000",
"title": "Foundation",
"author": "Isaac Asimov",
"cover_image_path": "/covers/foundation.jpg",
"match_reason": "Matched rule: genre equals Science Fiction"
}
],
"total": 42
}
```
## Use Case
Test rules before creating a collection to verify correct book matching. This endpoint shows which books would be added without actually modifying any collections.
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid request (validation failed) |
| 401 | Authentication required |
| 500 | Internal server error |
## Try It Out
@@ -0,0 +1,63 @@
# Update Collection
Update collection details.
**Endpoint**: `PUT /api/collections/{id}`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| id | string (UUID) | Yes | Collection UUID |
## Request Body
All fields are optional. Include only fields you want to update.
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| name | string | No | Collection name (max 255 chars) |
| description | string | No | Collection description |
| color | string | No | Hex color code (e.g., "#FF5733") |
| icon | string | No | Emoji icon (e.g., "🚀", "📖") |
| auto_assign_rules | array | No | Array of rule objects (replaces existing rules) |
| view_settings | object | No | Per-device display preferences |
### Example Request
```json
{
"name": "Sci-Fi Favorites",
"description": "Updated description",
"color": "#7aa2f7",
"icon": "🌟"
}
```
## Response (200 OK)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sci-Fi Favorites",
"description": "Updated description",
"color": "#7aa2f7",
"icon": "🌟",
"auto_assign_rules": [],
"view_settings": {},
"created_at": "2026-02-01T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid request (validation failed) |
| 401 | Authentication required |
| 404 | Collection not found |
| 500 | Internal server error |
## Try It Out
+62
View File
@@ -0,0 +1,62 @@
# Get Device / Check Registration Status
Check device registration status or get device details.
**Endpoint**: `POST /api/devices/auth/status` or `GET /api/devices/{device_id}`
**Auth**: Not required for status check, Required for device details
**Content-Type**: `application/json` (for status check)
## Request Body (Status Check)
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| registration_id | string | Yes | Registration UUID |
### Example Request (Status Check)
```json
{
"registration_id": "registration-uuid"
}
```
## Response (200 OK)
```json
{
"status": "pending|approved|expired",
"auth_token": "device-bearer-token...",
"device_id": "uuid",
"sync_endpoints": {
"progress": "https://bookhoard.com/api/sync/progress",
"metadata": "https://bookhoard.com/api/sync/metadata",
"annotations": "https://bookhoard.com/api/sync/annotations"
}
}
```
## Response (200 OK) - Device Details
```json
{
"id": "uuid",
"device_name": "My Kobo Clara",
"device_type": "kobo",
"last_sync": "2026-01-31T10:00:00Z",
"last_seen": "2026-01-31T10:05:00Z",
"sync_enabled": true,
"auto_sync": true,
"sync_frequency_minutes": 5
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token (for device details) |
| 404 | Device or registration not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,48 @@
# List User Devices
Retrieve all devices registered to the current user.
**Endpoint**: `GET /api/devices`
**Auth**: Required
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/devices
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"devices": [
{
"id": "uuid",
"device_name": "My Kobo Clara",
"device_type": "kobo",
"last_sync": "2026-01-31T10:00:00Z",
"last_seen": "2026-01-31T10:05:00Z",
"sync_enabled": true,
"auto_sync": true,
"sync_frequency_minutes": 5
}
]
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,48 @@
# Register Device
Register a new device for sync.
**Endpoint**: `POST /api/devices/register`
**Auth**: Not required (device registration flow)
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| device_name | string | Yes | Device name |
| device_type | string | Yes | Device type: kobo, koreader, web, mobile |
| device_identifier | string | Yes | Hardware-specific ID |
### Example Request
```json
{
"device_name": "My Kobo Clara",
"device_type": "kobo",
"device_identifier": "hardware-specific-id"
}
```
## Response (201 Created)
```json
{
"device_id": "uuid",
"registration_id": "registration-uuid",
"auth_url": "https://bookhoard.com/devices/auth/confirm/abc123",
"qr_code": "data:image/png;base64,iVBORw0KG...",
"expires_in": 300
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid device data |
| 409 | Device already registered |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,41 @@
# Revoke Device
Revoke access to a device.
**Endpoint**: `DELETE /api/devices/{device_id}`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| device_id | string | Yes | Device UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
DELETE /api/devices/uuid
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (204 No Content)
Device revoked successfully.
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 403 | User does not own this device |
| 404 | Device not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,66 @@
# Create Highlight
Create a new highlight for a media item.
**Endpoint**: `POST /api/media-items/{media_id}/highlights`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| selection_text | string | Yes | Highlighted text |
| start_position | string | No | Start position (e.g., epubcfi) |
| end_position | string | No | End position (e.g., epubcfi) |
| color | string | No | Highlight color (hex, default: "#ffff00") |
| percentage_start | float | No | Start percentage (0-1) |
| percentage_end | float | No | End percentage (0-1) |
### Example Request
```json
{
"selection_text": "Highlighted text...",
"start_position": "epubcfi(/6/4/2:15)",
"end_position": "epubcfi(/6/4/2:20)",
"color": "#ffff00",
"percentage_start": 0.45,
"percentage_end": 0.47
}
```
## Response (201 Created)
```json
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"selection_text": "Highlighted text...",
"start_position": "epubcfi(/6/4/2:15)",
"end_position": "epubcfi(/6/4/2:20)",
"color": "#ffff00",
"percentage_start": 0.45,
"percentage_end": 0.47,
"created_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid highlight data |
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,41 @@
# Delete Highlight
Delete a highlight.
**Endpoint**: `DELETE /api/media-items/highlights/{highlight_id}`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| highlight_id | string | Yes | Highlight UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
DELETE /api/media-items/highlights/uuid
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (204 No Content)
Highlight deleted successfully.
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 403 | User does not own this highlight |
| 404 | Highlight not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,61 @@
# Get Highlights
Retrieve all highlights for a specific media item.
**Endpoint**: `GET /api/media-items/{media_id}/highlights`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/media-items/uuid/highlights
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"highlights": [
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"selection_text": "Highlighted text passage...",
"start_position": "epubcfi(/6/4/2:15)",
"end_position": "epubcfi(/6/4/2:20)",
"color": "#ffff00",
"percentage_start": 0.45,
"percentage_end": 0.47,
"character_start": 15432,
"character_end": 15480,
"epubcfi_start": "epubcfi(/6/4/2:15)",
"epubcfi_end": "epubcfi(/6/4/2:20)",
"created_at": "2026-01-31T10:00:00Z"
}
]
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,55 @@
# Update Highlight
Update an existing highlight.
**Endpoint**: `PUT /api/media-items/highlights/{highlight_id}`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| highlight_id | string | Yes | Highlight UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| selection_text | string | No | Updated highlighted text |
| color | string | No | Updated highlight color (hex) |
### Example Request
```json
{
"selection_text": "Updated text",
"color": "#00ff00"
}
```
## Response (200 OK)
```json
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"selection_text": "Updated text",
"color": "#00ff00",
"updated_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid highlight data |
| 401 | Invalid or expired token |
| 403 | User does not own this highlight |
| 404 | Highlight not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,51 @@
# Add Library Folder
Add a folder to an existing library (Admin only).
**Endpoint**: `POST /api/libraries/{library_id}/folders`
**Auth**: Required (Admin only)
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| library_id | string | Yes | Library UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| folder_path | string | Yes | Absolute path to folder |
### Example Request
```json
{
"folder_path": "/path/to/comics"
}
```
## Response (201 Created)
```json
{
"id": "uuid",
"library_id": "uuid",
"folder_path": "/path/to/comics",
"created_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid folder path |
| 401 | Invalid or expired token |
| 403 | User is not an admin |
| 404 | Library not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,49 @@
# Create Library
Create a new library (Admin only).
**Endpoint**: `POST /api/libraries`
**Auth**: Required (Admin only)
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| name | string | Yes | Library name |
| description | string | No | Library description |
| type | string | Yes | Library type (e.g., "ebooks", "comics", "audiobooks") |
### Example Request
```json
{
"name": "Comics Collection",
"description": "Digital comics",
"type": "comics"
}
```
## Response (201 Created)
```json
{
"id": "uuid",
"name": "Comics Collection",
"description": "Digital comics",
"type": "comics",
"created_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid input data |
| 401 | Invalid or expired token |
| 403 | User is not an admin |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,55 @@
# Get Library Details
Retrieve details of a specific library.
**Endpoint**: `GET /api/libraries/{library_id}`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| library_id | string | Yes | Library UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/libraries/uuid-here
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"id": "uuid",
"name": "My Ebooks",
"description": "Ebook collection",
"type": "ebooks",
"folders": [
{
"id": "uuid",
"folder_path": "/path/to/ebooks"
}
],
"is_visible": true
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 403 | User does not have access to this library |
| 404 | Library not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,45 @@
# Get Visible Libraries
Retrieve all libraries visible to the current user.
**Endpoint**: `GET /api/libraries/visible`
**Auth**: Required
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/libraries/visible
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"libraries": [
{
"id": "uuid",
"name": "My Ebooks",
"description": "Ebook collection",
"type_name": "ebooks",
"is_visible": true
}
]
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,48 @@
# Set Library Visibility
Set library visibility for a specific user (Admin only).
**Endpoint**: `POST /api/libraries/visibility`
**Auth**: Required (Admin only)
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| user_id | string | Yes | User UUID |
| library_id | string | Yes | Library UUID |
| is_visible | boolean | Yes | Whether library is visible to user |
### Example Request
```json
{
"user_id": "user-uuid",
"library_id": "library-uuid",
"is_visible": true
}
```
## Response (200 OK)
```json
{
"user_id": "user-uuid",
"library_id": "library-uuid",
"is_visible": true
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid input data |
| 401 | Invalid or expired token |
| 403 | User is not an admin |
| 404 | User or library not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,41 @@
# Delete Media Item
Delete a media item from the library (Admin only).
**Endpoint**: `DELETE /api/media-items/{media_id}`
**Auth**: Required (Admin only)
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
DELETE /api/media-items/uuid-here
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (204 No Content)
Media item deleted successfully.
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 403 | User is not an admin |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,67 @@
# Filter & Sort Media Items
Filter and sort media items with advanced criteria.
**Endpoint**: `POST /api/media-items/filter`
**Auth**: Required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| library_id | string | Yes | Library UUID |
| author_filter | string | No | Filter by author name |
| series_filter | string | No | Filter by series name |
| genre_filter | string | No | Filter by genre |
| year_min | integer | No | Minimum copyright year |
| year_max | integer | No | Maximum copyright year |
| has_cover | boolean | No | Filter by cover image existence |
| sort | string | No | Sort field and order (e.g., "title ASC", "created_at DESC") |
| limit | integer | No | Number of results (default 20) |
| offset | integer | No | Number to skip |
### Example Request
```json
{
"library_id": "uuid",
"author_filter": "Rowling",
"series_filter": "Harry Potter",
"genre_filter": "Fantasy",
"year_min": 1997,
"year_max": 2007,
"has_cover": true,
"sort": "title ASC",
"limit": 20,
"offset": 0
}
```
## Response (200 OK)
```json
{
"media_items": [
{
"id": "uuid",
"title": "Book Title",
"author": "Author Name",
"match_score": 0.95
}
],
"total": 7
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid filter parameters |
| 401 | Invalid or expired token |
| 403 | User does not have access to this library |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,61 @@
# Get Media Item
Retrieve details of a specific media item.
**Endpoint**: `GET /api/media-items/{media_id}`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/media-items/uuid-here
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"id": "uuid",
"library_id": "uuid",
"title": "Book Title",
"author": "Author Name",
"description": "Book description",
"file_path": "/path/to/book.epub",
"file_size": 1024000,
"mime_type": "application/epub+zip",
"cover_image_path": "/path/to/cover.jpg",
"series": "Series Name",
"series_number": 1,
"tags": "sci-fi, space opera",
"language": "en",
"page_count": 350,
"genre": "Science Fiction",
"copyright_year": 2023,
"created_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 403 | User does not have access to this media item |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,68 @@
# List Media Items
Retrieve a paginated list of media items from a library.
**Endpoint**: `GET /api/media-items`
**Auth**: Required
## Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| library_id | string | Yes | Library UUID |
| limit | integer | No | Number of items to return (max 100, default 20) |
| offset | integer | No | Number of items to skip |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/media-items?library_id=uuid&limit=20&offset=0
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"media_items": [
{
"id": "uuid",
"library_id": "uuid",
"title": "Book Title",
"author": "Author Name",
"description": "Book description",
"file_path": "/path/to/book.epub",
"file_size": 1024000,
"mime_type": "application/epub+zip",
"cover_image_path": "/path/to/cover.jpg",
"series": "Series Name",
"series_number": 1,
"tags": "sci-fi, space opera",
"language": "en",
"page_count": 350,
"genre": "Science Fiction",
"copyright_year": 2023,
"created_at": "2026-01-31T10:00:00Z"
}
],
"total": 100
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid query parameters |
| 401 | Invalid or expired token |
| 403 | User does not have access to this library |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,54 @@
# Search Media Items
Search for media items by title, author, or description.
**Endpoint**: `GET /api/media-items/search`
**Auth**: Required
## Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| q | string | Yes | Search query (minimum 2 characters) |
| limit | integer | No | Number of results (default 20) |
| offset | integer | No | Number to skip |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/media-items/search?q=Harry+Potter&limit=20&offset=0
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"results": [
{
"id": "uuid",
"title": "Book Title",
"author": "Author Name",
"match_score": 0.95
}
],
"total": 15
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid search query (too short) |
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,62 @@
# Update Media Item
Update media item metadata (Admin only).
**Endpoint**: `PUT /api/media-items/{media_id}`
**Auth**: Required (Admin only)
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| title | string | No | Updated title |
| author | string | No | Updated author |
| description | string | No | Updated description |
| series | string | No | Series name |
| series_number | integer | No | Number in series |
### Example Request
```json
{
"title": "Updated Title",
"author": "Updated Author",
"description": "Updated description",
"series": "Series",
"series_number": 2
}
```
## Response (200 OK)
```json
{
"id": "uuid",
"title": "Updated Title",
"author": "Updated Author",
"description": "Updated description",
"series": "Series",
"series_number": 2,
"updated_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid input data |
| 401 | Invalid or expired token |
| 403 | User is not an admin |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+60
View File
@@ -0,0 +1,60 @@
# Create Note
Create a new note for a media item.
**Endpoint**: `POST /api/media-items/{media_id}/notes`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| content | string | Yes | Note content |
| position | string | No | Location reference (e.g., epubcfi) |
| percentage_location | float | No | Location as percentage (0-1) |
| epubcfi_location | string | No | EPUB CFI location |
### Example Request
```json
{
"content": "This is a note",
"position": "epubcfi(/6/4/2:15)",
"percentage_location": 0.45,
"epubcfi_location": "epubcfi(/6/4/2:15)"
}
```
## Response (201 Created)
```json
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"content": "This is a note",
"position": "epubcfi(/6/4/2:15)",
"percentage_location": 0.45,
"epubcfi_location": "epubcfi(/6/4/2:15)",
"created_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid note data |
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+41
View File
@@ -0,0 +1,41 @@
# Delete Note
Delete a note.
**Endpoint**: `DELETE /api/media-items/notes/{note_id}`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| note_id | string | Yes | Note UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
DELETE /api/media-items/notes/uuid
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (204 No Content)
Note deleted successfully.
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 403 | User does not own this note |
| 404 | Note not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+58
View File
@@ -0,0 +1,58 @@
# Get Notes
Retrieve all notes for a specific media item.
**Endpoint**: `GET /api/media-items/{media_id}/notes`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/media-items/uuid/notes
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"notes": [
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"content": "This is an interesting passage...",
"position": "epubcfi(/6/4/2:15)",
"percentage_location": 0.45,
"character_start": 15432,
"character_end": 15480,
"epubcfi_location": "epubcfi(/6/4/2:15)",
"created_at": "2026-01-31T10:00:00Z",
"updated_at": "2026-01-31T10:00:00Z"
}
]
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+55
View File
@@ -0,0 +1,55 @@
# Update Note
Update an existing note.
**Endpoint**: `PUT /api/media-items/notes/{note_id}`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| note_id | string | Yes | Note UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| content | string | No | Updated note content |
| position | string | No | Updated location reference |
### Example Request
```json
{
"content": "Updated note content",
"position": "epubcfi(/6/4/2:20)"
}
```
## Response (200 OK)
```json
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"content": "Updated note content",
"position": "epubcfi(/6/4/2:20)",
"updated_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid note data |
| 401 | Invalid or expired token |
| 403 | User does not own this note |
| 404 | Note not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+72
View File
@@ -0,0 +1,72 @@
# OPDS Acquisition
Download books and list available formats.
## Download Book with Format Conversion
**Endpoint**: `GET /opds/devices/{deviceId}/download/{bookId}`
**Auth**: Device token required
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| format | string | No | Book format: `epub` (default), `kepub` |
### Example Request
```http
GET /opds/devices/kobo-id/download/uuid-123?format=kepub
```
### Response (200 OK)
**Headers:**
- `Content-Type`: `application/epub+zip` or `application/vnd.kobo+xml+zip`
- `Content-Disposition`: attachment; filename="The Hobbit.epub"
- `X-Bookhoard-UUID`: uuid-123
- `X-Bookhoard-SHA256`: abc123... (original hash)
- `X-Bookhoard-KEPUB-SHA256`: xyz789... (KEPUB hash if format=kepub)
**Body:** Book file binary data
## List Available Formats
**Endpoint**: `GET /opds/devices/{deviceId}/formats/{bookId}`
**Auth**: Device token required
### Example Request
```http
GET /opds/devices/kobo-id/formats/uuid-123
```
### Response (200 OK)
```json
{
"media_item_id": "uuid-123",
"formats": [
{
"format_type": "epub",
"file_path": "/path/to/book.epub",
"file_sha256": "abc123...",
"file_size_bytes": 2456789,
"mime_type": "application/epub+zip",
"available": true
},
{
"format_type": "kepub",
"file_path": "/cache/book.kepub.epub",
"file_sha256": "xyz789...",
"file_size_bytes": 2478932,
"mime_type": "application/vnd.kobo+xml+zip",
"available": true
}
]
}
```
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+81
View File
@@ -0,0 +1,81 @@
# OPDS Feeds
Bookhoard provides OPDS 1.2 feeds for device compatibility.
## Get Device Catalog
**Endpoint**: `GET /opds/devices/{deviceId}/catalog`
**Auth**: Device token required
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| page | integer | No | Page number (default: 1) |
| per_page | integer | No | Items per page (default: 50, max: 200) |
### Example Request
```http
GET /opds/devices/kobo-id/catalog?page=1&per_page=50
```
### Response (200 OK - OPDS 1.2 XML)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="http://opds-spec.org/2010/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<id>urn:uuid:device-id</id>
<title>Bookhoard Library</title>
<updated>2026-02-01T12:00:00Z</updated>
<link rel="self" href="http://localhost:8765/opds/devices/kobo-id/catalog"/>
<link rel="search" href="http://localhost:8765/opds/devices/kobo-id/search"/>
<link rel="start" href="http://localhost:8765/opds/devices/kobo-id/nav"/>
<entry>
<id>urn:uuid:bookhoard-uuid-123</id>
<dc:title>The Hobbit</dc:title>
<dc:creator>J.R.R. Tolkien</dc:creator>
<updated>2026-02-01T10:00:00Z</updated>
<link href="http://localhost:8765/opds/devices/kobo-id/download/uuid-123"
type="application/epub+zip"
rel="http://opds-spec.org/acquisition/open-access"/>
<link href="http://localhost:8765/opds/devices/kobo-id/download/uuid-123?format=kepub"
type="application/vnd.kobo+xml+zip"
rel="alternate"/>
<dc:identifier id="bookhoard">uuid-123</dc:identifier>
<meta property="bookhoard:sha256">abc123...</meta>
</entry>
</feed>
```
## Search OPDS Catalog
**Endpoint**: `GET /opds/devices/{deviceId}/search`
**Auth**: Device token required
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| q | string | Yes | Search query |
### Example Request
```http
GET /opds/devices/kobo-id/search?q=Hobbit
```
### Response (200 OK - OPDS 1.2 XML with search results)
Returns OPDS feed with matching books.
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+56
View File
@@ -0,0 +1,56 @@
# OPDS Publication
OPDS navigation and publication feeds.
## Navigation Feed
**Endpoint**: `GET /opds/devices/{deviceId}/nav`
**Auth**: Device token required
### Example Request
```http
GET /opds/devices/kobo-id/nav
```
### Response (200 OK - OPDS 1.2 Navigation XML)
Returns OPDS navigation feed with links to:
- Root catalog
- Search
- Collections/shelves
- Filtered views (by author, series, etc.)
## Publication Feeds by Collection
**Endpoint**: `GET /opds/devices/{deviceId}/collections/{collectionId}`
**Auth**: Device token required
### Example Request
```http
GET /opds/devices/kobo-id/collections/collection-uuid
```
### Response (200 OK - OPDS 1.2 XML)
Returns OPDS feed with books in the specified collection.
## Publication Feeds by Shelf
**Endpoint**: `GET /opds/devices/{deviceId}/shelves/{shelfName}`
**Auth**: Device token required
### Example Request
```http
GET /opds/devices/kobo-id/shelves/Favorites
```
### Response (200 OK - OPDS 1.2 XML)
Returns OPDS feed with books on the specified device shelf.
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,40 @@
# Delete Reading Progress
Delete reading progress for a media item.
**Endpoint**: `DELETE /api/media-items/{media_id}/progress`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
DELETE /api/media-items/uuid/progress
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (204 No Content)
Progress deleted successfully.
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,56 @@
# Get Reading Progress
Retrieve reading progress for a specific media item.
**Endpoint**: `GET /api/media-items/{media_id}/progress`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/media-items/uuid/progress
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"media_item_id": "uuid",
"user_id": "uuid",
"current_page": 45,
"total_pages": 200,
"percentage": 0.225,
"character_offset": 15432,
"epubcfi": "epubcfi(/6/4/2:15)",
"chapter": 3,
"chapter_progress": 0.5,
"last_read_at": "2026-01-31T10:00:00Z",
"format_group": "reflowable",
"viewport_y": 0.12,
"zoom_level": 1.0
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,72 @@
# Update Reading Progress
Update reading progress for a media item. This will sync across all devices via WebSocket.
**Endpoint**: `PUT /api/media-items/{media_id}/progress`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| source | string | Yes | Progress source (e.g., "web", "koreader", "kobo") |
| location | object | Yes | Location information |
| location.percentage | float | No | Progress percentage (0-1) |
| location.epubcfi | string | No | EPUB CFI location |
| location.character | integer | No | Character offset |
| location.chapter | integer | No | Chapter number |
| location.page | integer | No | Current page |
| location.total_pages | integer | No | Total pages |
| device_metadata | object | No | Device metadata |
| device_metadata.device_type | string | No | Device type |
| device_metadata.user_agent | string | No | User agent string |
### Example Request
```json
{
"source": "web",
"location": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"chapter": 3,
"page": 89,
"total_pages": 200
},
"device_metadata": {
"device_type": "web",
"user_agent": "Mozilla/5.0..."
}
}
```
## Response (200 OK)
```json
{
"sync_status": "success",
"progress_updated": true,
"devices_notified": ["device-1", "device-2"],
"broadcast": true
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid location data |
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,52 @@
# Create Rating
Set a rating for a media item. Creates a new rating or updates an existing one.
**Endpoint**: `POST /api/media-items/{media_id}/rating`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| rating | integer | Yes | Rating from 1-10 |
### Example Request
```json
{
"rating": 8
}
```
## Response (201 Created)
```json
{
"rating": 8,
"user_id": "uuid",
"media_item_id": "uuid",
"created_at": "2026-01-31T10:00:00Z"
}
```
**Rating Scale**: 1-10 (odd numbers = half-stars: 1=0.5★, 2=1★, 3=1.5★, ..., 9=4.5★, 10=5★)
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid rating (must be 1-10) |
| 401 | Invalid or expired token |
| 404 | Media item not found |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+48
View File
@@ -0,0 +1,48 @@
# Get Rating
Retrieve the current user's rating for a media item.
**Endpoint**: `GET /api/media-items/{media_id}/rating`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/media-items/uuid/rating
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"rating": 8,
"user_id": "uuid",
"media_item_id": "uuid"
}
```
**Rating Scale**: 1-10 (odd numbers = half-stars: 1=0.5★, 2=1★, 3=1.5★, ..., 9=4.5★, 10=5★)
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
| 404 | Media item not found or no rating set |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+103
View File
@@ -0,0 +1,103 @@
# Kobo Sync Protocol
Kobo uses a proprietary sync protocol with JSON payloads.
## Kobo Markup Sync
**Endpoint**: `POST /api/sync/kobo/markup`
**Auth**: Device token required
**Content-Type**: `application/json`
### Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer device token |
| x-kobo-device | string | Yes | JSON device info |
| Content-Type | string | Yes | application/json |
### Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| ReadingSync | array | No | Array of reading progress data |
| ReadingSync[].ContentId | string | Yes | Book UUID |
| ReadingSync[].PercentRead | float | Yes | Progress percentage (0-100) |
| ReadingSync[].EntitlementId | string | Yes | Kobo entitlement ID |
| ReadingSync[].RemainingTimeMinutes | integer | No | Estimated remaining time |
| ReadingSync[].LastModified | string | Yes | ISO 8601 timestamp |
| BookmarkSync | array | No | Array of bookmarks/highlights |
### Example Request
```http
POST /api/sync/kobo/markup
Authorization: Bearer device-token
x-kobo-device: {"DeviceId":"device-id","Model":"Kobo Clara"}
Content-Type: application/json
{
"ReadingSync": [
{
"ContentId": "book-uuid",
"PercentRead": 45.6,
"EntitlementId": "entitlement-id",
"RemainingTimeMinutes": 120,
"LastModified": "2026-01-30T20:00:00Z"
}
],
"BookmarkSync": [
{
"ContentId": "book-uuid",
"BookmarkText": "highlighted text",
"BookmarkType": "annotation",
"BookmarkTitle": "Chapter 3"
}
]
}
```
### Response (200 OK)
```json
{
"Status": "Success",
"MarkupsSynced": 5,
"BookmarksSynced": 3
}
```
## Kobo Library Fetch
**Endpoint**: `GET /api/sync/kobo/library`
**Auth**: Device token required
### Example Request
```http
GET /api/sync/kobo/library
Authorization: Bearer device-token
```
### Response (200 OK)
```json
{
"library_sync": [
{
"ContentId": "book-uuid",
"ContentType": "6",
"Title": "Book Title",
"Author": "Author Name",
"PercentRead": 42.3,
"PagesRemaining": 115,
"BookmarkCount": 3,
"LastModified": "2026-01-30T20:00:00Z"
}
]
}
```
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,125 @@
# KOReader Sync Protocol
KOReader uses a custom JSON-based sync protocol.
## KOReader Progress Sync
**Endpoint**: `POST /api/sync/koreader/progress`
**Auth**: Device token required
**Content-Type**: `application/json`
### Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer device token |
| Content-Type | string | Yes | application/json |
### Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| library_id | string | No | Library UUID |
| books | array | Yes | Array of book sync data |
| books[].uuid | string | Yes | Book UUID |
| books[].title | string | Yes | Book title |
| books[].authors | array | Yes | Array of author names |
| books[].progress | float | Yes | Progress percentage (0-1) |
| books[].percentage | float | Yes | Progress percentage (0-1) |
| books[].last_read | string | Yes | ISO 8601 timestamp |
| books[].chapter | integer | No | Current chapter |
| books[].epubcfi | string | No | EPUB CFI location |
| books[].character | integer | No | Character offset |
| books[].bookmarks | array | No | Array of bookmarks/highlights |
### Example Request
```json
{
"library_id": "optional-uuid",
"books": [
{
"uuid": "book-uuid",
"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,
"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"
}
],
"highlights": [],
"notes": []
}
]
}
```
### Response (202 Accepted)
```json
{
"sync_status": "accepted",
"books_synced": 1,
"conflicts": [
{
"book_uuid": "book-uuid",
"conflict_type": "progress_mismatch",
"device_progress": 0.45,
"server_progress": 0.42,
"resolution": "device_wins"
}
]
}
```
## KOReader Metadata Fetch
**Endpoint**: `GET /api/sync/koreader/metadata/{book_uuid}`
**Auth**: Device token required
### Example Request
```http
GET /api/sync/koreader/metadata/book-uuid
Authorization: Bearer device-token
```
### Response (200 OK)
```json
{
"uuid": "book-uuid",
"title": "Book Title",
"authors": ["Author Name"],
"progress": {
"percentage": 0.42,
"character": 15432,
"epubcfi": "epubcfi(/6/4/2:15)",
"chapter": 3,
"chapter_progress": 0.234
},
"annotations": {
"highlights": [...],
"notes": [...],
"bookmarks": [...]
},
"last_sync": "2026-01-30T20:00:00Z"
}
```
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,39 @@
# Change Password
Change the current user's password.
**Endpoint**: `PUT /api/users/me/password`
**Auth**: Required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| current_password | string | Yes | Current password |
| new_password | string | Yes | New password (min 8 chars) |
### Example Request
```json
{
"current_password": "oldPassword",
"new_password": "NewSecureP@ss123!"
}
```
## Response (204 No Content)
Password changed successfully.
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid input or weak password |
| 401 | Current password is incorrect |
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+45
View File
@@ -0,0 +1,45 @@
# Get Current User Profile
Retrieve the current authenticated user's profile.
**Endpoint**: `GET /api/users/me`
**Auth**: Required
## Request Headers
| Header | Type | Required | Description |
|--------|------|-----------|-------------|
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"id": "uuid",
"email": "user@example.com",
"username": "john",
"first_name": "John",
"last_name": "Doe",
"theme": "tokyo-night",
"role": "user",
"max_devices": 10,
"created_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
@@ -0,0 +1,50 @@
# Update Profile
Update the current user's profile information.
**Endpoint**: `PUT /api/users/me/profile`
**Auth**: Required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| first_name | string | No | User's first name |
| last_name | string | No | User's last name |
### Example Request
```json
{
"first_name": "John",
"last_name": "Smith"
}
```
## Response (200 OK)
```json
{
"id": "uuid",
"email": "user@example.com",
"username": "john",
"first_name": "John",
"last_name": "Smith",
"theme": "tokyo-night",
"role": "user",
"max_devices": 10,
"created_at": "2026-01-31T10:00:00Z"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid input data |
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+44
View File
@@ -0,0 +1,44 @@
# Update Theme
Update the current user's theme preference.
**Endpoint**: `PUT /api/users/me/theme`
**Auth**: Required
**Content-Type**: `application/json`
## Request Body
| Field | Type | Required | Description |
|--------|------|-----------|-------------|
| theme | string | Yes | Theme name (e.g., "tokyo-night", "dracula") |
### Example Request
```json
{
"theme": "dracula"
}
```
## Response (200 OK)
```json
{
"id": "uuid",
"email": "user@example.com",
"username": "john",
"theme": "dracula",
"role": "user"
}
```
## Error Responses
| Code | Description |
|------|-------------|
| 400 | Invalid theme name |
| 401 | Invalid or expired token |
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+109
View File
@@ -0,0 +1,109 @@
# WebSocket Protocol
Real-time sync events broadcast to connected clients.
## Connect to WebSocket
**Endpoint**: `WS /ws/sync?token=<token>`
### Connection Parameters
| Parameter | Type | Required | Description |
|-----------|------|-----------|-------------|
| token | string | Yes | JWT authentication token |
### Example Connection
```javascript
const ws = new WebSocket('wss://bookhoard.com/ws/sync?token=eyJhbG...');
```
## Message Format
All messages are JSON objects with a `type` field.
### Client → Server Messages
#### Ping (Heartbeat)
```json
{
"type": "ping"
}
```
Keep connection alive. Server responds with `pong`.
### Server → Client Messages
#### Progress Update
```json
{
"type": "progress_update",
"timestamp": "2026-01-31T10:00:00Z",
"data": {
"book_id": "uuid",
"progress": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"chapter": 3
},
"annotations": {}
},
"source_device": {
"id": "device-uuid",
"name": "My Kobo",
"type": "kobo"
}
}
```
Broadcast when any device updates reading progress.
#### Conflict Detected
```json
{
"type": "conflict",
"timestamp": "2026-01-31T10:00:00Z",
"data": {
"book_id": "uuid",
"conflict_id": "uuid",
"conflict_type": "progress"
}
}
```
Broadcast when a sync conflict is detected.
#### Pong
```json
{
"type": "pong"
}
```
Server response to client `ping`.
## Connection Management
- **Heartbeat**: Send `ping` every 30 seconds
- **Reconnect**: Use exponential backoff if connection drops
- **Authentication**: Token must be valid for connection
- **Rate Limits**: 60 messages/minute per connection
## Error Handling
```json
{
"type": "error",
"message": "Invalid token",
"code": "AUTH_FAILED"
}
```
## Try It Out
<!-- API Explorer will be inserted here in Phase 3 -->
+493
View File
@@ -0,0 +1,493 @@
# Collections API Documentation
Complete API reference for collection management endpoints.
**Base Path**: `/api/collections`
---
## Overview
Collections allow you to organize your books into custom categories with:
- **Auto-assignment rules**: Automatically add books matching criteria
- **View settings**: Per-device display preferences
- **Shelf mappings**: Sync to device-specific shelves (Kobo, KOReader)
---
## Endpoints
### List All Collections
**Endpoint**: `GET /api/collections`
**Authentication**: Required
**Description**: Get all collections for the authenticated user
**Response** (200 OK):
```json
{
"collections": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Science Fiction",
"description": "My sci-fi collection",
"color": "#FF5733",
"icon": "🚀",
"auto_assign_rules": [
{
"field": "genre",
"operator": "equals",
"value": "Science Fiction",
"priority": 1,
"enabled": true
}
],
"view_settings": {
"kobo": {
"view_mode": "grid",
"sort_order": "name",
"items_per_page": 24
}
},
"created_at": "2026-02-01T10:00:00Z"
}
],
"total": 1
}
```
### Create Collection
**Endpoint**: `POST /api/collections`
**Authentication**: Required
**Description**: Create a new collection
**Request Body**:
```json
{
"name": "To Read",
"description": "Books I want to read soon",
"color": "#00FF00",
"icon": "📖",
"auto_assign_rules": [
{
"field": "tags",
"operator": "contains",
"value": "to-read",
"priority": 1,
"enabled": true
}
],
"view_settings": {
"kobo": {
"view_mode": "grid"
},
"koreader": {
"view_mode": "list"
}
}
}
```
**Fields**:
- `name` (required): Collection name (max 255 chars)
- `description` (optional): Collection description
- `color` (optional): Hex color code (e.g., "#FF5733")
- `icon` (optional): Emoji icon (e.g., "🚀", "📖")
- `auto_assign_rules` (optional): Array of rule objects
- `view_settings` (optional): Per-device display preferences
**Rule Object**:
- `field`: Field to match on (genre, author, series, language, publisher, copyright_year, tags)
- `operator`: Comparison operator (equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than)
- `value`: Value to compare against
- `priority`: Rule priority (1 = highest)
- `enabled`: Whether rule is active
**Response** (201 Created): Collection object
### Get Collection Details
**Endpoint**: `GET /api/collections/{id}`
**Authentication**: Required
**Description**: Get single collection with all books
**Path Parameters**:
- `id`: Collection UUID
**Response** (200 OK):
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Science Fiction",
"description": "My sci-fi collection",
"color": "#FF5733",
"icon": "🚀",
"books": [
{
"media_item_id": "660e8400-e29b-41d4-a716-446655440000",
"title": "Foundation",
"author": "Isaac Asimov",
"cover_image_path": "/covers/foundation.jpg"
}
],
"auto_assign_rules": [],
"view_settings": {},
"created_at": "2026-02-01T10:00:00Z"
}
```
### Update Collection
**Endpoint**: `PUT /api/collections/{id}`
**Authentication**: Required
**Description**: Update collection details
**Path Parameters**:
- `id`: Collection UUID
**Request Body**: All fields are optional
```json
{
"name": "Sci-Fi Favorites",
"description": "Updated description",
"color": "#BLUE",
"icon": "🌟"
}
```
**Response** (200 OK): Updated collection object
### Delete Collection
**Endpoint**: `DELETE /api/collections/{id}`
**Authentication**: Required
**Description**: Delete a collection (books are NOT deleted)
**Path Parameters**:
- `id`: Collection UUID
**Response** (204 No Content)
---
## Collection Books
### Add Books to Collection
**Endpoint**: `POST /api/collections/{id}/books`
**Authentication**: Required
**Description**: Add one or more books to a collection
**Path Parameters**:
- `id`: Collection UUID
**Request Body**:
```json
{
"book_ids": [
"660e8400-e29b-41d4-a716-446655440000",
"770e8400-e29b-41d4-a716-446655440000",
"880e8400-e29b-41d4-a716-446655440000"
]
}
```
**Response** (204 No Content)
### Remove Book from Collection
**Endpoint**: `DELETE /api/collections/{id}/books/{bookId}`
**Authentication**: Required
**Description**: Remove a single book from a collection
**Path Parameters**:
- `id`: Collection UUID
- `bookId`: Media Item UUID
**Response** (204 No Content)
### Bulk Remove Books
**Endpoint**: `POST /api/collections/{id}/books/bulk-remove`
**Authentication**: Required
**Description**: Remove multiple books at once (efficient)
**Path Parameters**:
- `id`: Collection UUID
**Request Body**:
```json
{
"book_ids": [
"660e8400-e29b-41d4-a716-446655440000",
"770e8400-e29b-41d4-a716-446655440000"
]
}
```
**Response** (200 OK):
```json
{
"removed": 2,
"total": 2
}
```
**Example**: Removing 2 books, both successfully removed
### Get Collection Books
**Endpoint**: `GET /api/collections/{id}/books`
**Authentication**: Required
**Description**: Get all books in a collection
**Path Parameters**:
- `id`: Collection UUID
**Query Parameters**:
- `limit` (optional): Number of books to return (default: 50)
- `offset` (optional): Number of books to skip (default: 0)
**Response** (200 OK):
```json
{
"books": [
{
"media_item_id": "660e8400-e29b-41d4-a716-446655440000",
"title": "Foundation",
"author": "Isaac Asimov",
"cover_image_path": "/covers/foundation.jpg",
"added_at": "2026-02-01T10:00:00Z"
}
],
"total": 1
}
```
---
## Collection Rules
### Test Collection Rules
**Endpoint**: `POST /api/collections/test-rules`
**Authentication**: Required
**Description**: Test which books would match given rules (without saving)
**Request Body**:
```json
{
"rules": [
{
"field": "genre",
"operator": "equals",
"value": "Science Fiction"
},
{
"field": "author",
"operator": "contains",
"value": "Asimov"
}
]
}
```
**Supported Fields**:
- `genre`: Book genre
- `author`: Book author
- `series`: Book series name
- `language`: Book language
- `publisher`: Publisher name
- `copyright_year`: Publication year (numeric comparison)
- `tags`: Book tags
**Supported Operators**:
- `equals`: Exact match
- `not_equals`: Not equal
- `contains`: Contains substring (case-insensitive)
- `not_contains`: Does not contain
- `starts_with`: Starts with (case-insensitive)
- `ends_with`: Ends with (case-insensitive)
- `greater_than`: Greater than (numeric)
- `less_than`: Less than (numeric)
**Response** (200 OK):
```json
{
"matches": [
{
"media_item_id": "660e8400-e29b-41d4-a716-446655440000",
"title": "Foundation",
"author": "Isaac Asimov",
"cover_image_path": "/covers/foundation.jpg",
"match_reason": "Matched rule: genre equals Science Fiction"
}
],
"total": 42
}
```
**Use Case**: Test rules before creating collection to verify correct book matching
---
## Device Shelf Mappings
### Get Device Shelf Mappings
**Endpoint**: `GET /api/devices/{deviceId}/collections`
**Authentication**: Required
**Description**: Get all collection-to-shelf mappings for a device
**Path Parameters**:
- `deviceId`: Device UUID
**Response** (200 OK):
```json
{
"mappings": [
{
"id": "990e8400-e29b-41d4-a716-446655440000",
"collection_id": "550e8400-e29b-41d4-a716-446655440000",
"collection_name": "Science Fiction",
"device_shelf_name": "Sci-Fi",
"sync_direction": "bidirectional",
"created_at": "2026-02-01T10:00:00Z"
}
]
}
```
### Create Shelf Mapping
**Endpoint**: `POST /api/devices/{deviceId}/collections`
**Authentication**: Required
**Description**: Map a collection to a device shelf
**Path Parameters**:
- `deviceId`: Device UUID
**Request Body**:
```json
{
"collection_id": "550e8400-e29b-41d4-a716-446655440000",
"device_shelf_name": "Sci-Fi",
"sync_direction": "bidirectional"
}
```
**Sync Directions**:
- `bidirectional`: Sync both ways between Bookhoard and device
- `book_to_hoard`: Bookhoard → Device only
- `device_to_hoard`: Device → Bookhoard only
- `none`: No sync (mapping only)
**Response** (201 Created): Mapping object
### Update Shelf Mapping
**Endpoint**: `PUT /api/devices/{deviceId}/collections/{collectionId}`
**Authentication**: Required
**Description**: Update existing shelf mapping
**Path Parameters**:
- `deviceId`: Device UUID
- `collectionId`: Collection UUID
**Request Body**:
```json
{
"device_shelf_name": "Science Fiction",
"sync_direction": "book_to_hoard"
}
```
**Response** (200 OK): Updated mapping object
### Delete Shelf Mapping
**Endpoint**: `DELETE /api/devices/{deviceId}/collections/{collectionId}`
**Authentication**: Required
**Description**: Remove shelf mapping
**Path Parameters**:
- `deviceId`: Device UUID
- `collectionId`: Collection UUID
**Response** (204 No Content)
---
## Error Responses
All endpoints may return these errors:
**400 Bad Request**:
```json
{
"error": "invalid request: validation failed"
}
```
**401 Unauthorized**:
```json
{
"error": "authentication required"
}
```
**404 Not Found**:
```json
{
"error": "collection not found"
}
```
**500 Internal Server Error**:
```json
{
"error": "internal server error"
}
```
---
## Rate Limiting
- **Authenticated**: 100 requests per minute
- **Unauthenticated**: 10 requests per minute
Headers included:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1643723400
```
---
## Bruno Tests
Complete API tests available in `bruno/collections/`:
- `Get Collections.bru`
- `Create Collection.bru`
- `Get Collection.bru`
- `Update Collection.bru`
- `Delete Collection.bru`
- `Add Books to Collection.bru`
- `Remove Book from Collection.bru`
- `Get Book Collections.bru`
- `Test Collection Rules.bru`
- `Bulk Remove Books.bru`
Run tests:
```bash
bruno run bruno/collections/
```
---
**Last Updated**: 2026-02-01
**Version**: 1.0
+675
View File
@@ -0,0 +1,675 @@
# 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](#authentication)
2. [Connection](#connection)
3. [Message Format](#message-format)
4. [Message Types](#message-types)
5. [Client Implementation Guide](#client-implementation-guide)
6. [Examples](#examples)
7. [Troubleshooting](#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**:
```bash
curl -X POST http://localhost:8765/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password"}'
```
Response:
```json
{
"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**:
```javascript
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**:
```go
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**:
```python
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:
```javascript
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:
```javascript
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:
```json
{
"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**:
```json
{
"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**:
```json
{
"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**:
```json
{
"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**:
```json
{
"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**:
```json
{
"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:
```javascript
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:
```javascript
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:
```javascript
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
```javascript
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
```javascript
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)