From 18084de7a61794706e07fd3608d77b3ab8374bf5 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 24 Oct 2026 17:59:27 -0400 Subject: [PATCH] =?UTF-8?q?docs(book-matching,collections):=20Beam=20batch?= =?UTF-8?q?=2010=20=E2=80=94=2015=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WO3 sync internals resolved: the book-matching docs ARE the sync surface (books/query, link-book, bulk/auto-link, unlinked-books, suggestions, file-alias CRUD) + collections rule/test/bulk-assign/shelf-mapping surface. Dedupe resolved (Claude call): search_books.md declared GET /api/books/match but its BODY documents POST /api/sync/books/query — both routes exist and are distinct (query-param device protocol vs JSON body plugin protocol). Repointed search_books.md frontmatter to the POST route; get_book_matches.md keeps the GET. Claude registry: 15 entries (one pre-existing link-book entry deduped). Drift fixed by gate: auto-link request {confidence_threshold, limit} not {device_id, threshold}; bulk-link link objects {unlinked_book_id, media_item_id, confidence_score} not {device_id, book_id, ...}; responses {results,total,successful,failed}/{auto_linked,results}; added missing example-responses (bare delete, remove-rule is_smart-after semantics, bulk-assign per-book results). make test 9/9. --- .../api/book-matching/auto_link_books.md | 28 +++--- .../api/book-matching/bulk_link_books.md | 31 ++++--- .../book-matching/create_device_file_alias.md | 19 ++-- .../book-matching/delete_device_file_alias.md | 14 +++ .../book-matching/get_device_file_aliases.md | 13 ++- .../get_unlinked_book_suggestions.md | 25 +++++- .../api/book-matching/get_unlinked_books.md | 22 +++-- docs/developer/api/book-matching/link_book.md | 38 ++++++++ .../api/book-matching/search_books.md | 12 ++- .../book-matching/update_device_file_alias.md | 20 +++-- .../api/collections/add_auto_assign_rule.md | 22 +++-- docs/developer/api/collections/bulk_assign.md | 26 +++++- .../api/collections/create_shelf_mapping.md | 16 +++- .../collections/remove_auto_assign_rule.md | 18 ++++ docs/developer/api/collections/test_rule.md | 12 ++- internal/docs/schema.go | 90 +++++++++++++++++++ 16 files changed, 334 insertions(+), 72 deletions(-) diff --git a/docs/developer/api/book-matching/auto_link_books.md b/docs/developer/api/book-matching/auto_link_books.md index be137065..5a61cadb 100644 --- a/docs/developer/api/book-matching/auto_link_books.md +++ b/docs/developer/api/book-matching/auto_link_books.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/sync/auto-link-books +auth: jwt +area: book-matching +--- + + # Auto Link Books Automatically link books to media items based on matching metadata. @@ -15,27 +23,19 @@ Automatically link books to media items based on matching metadata. ### Example Request -```json +```json example-request { - "device_id": "550e8400-e29b-41d4-a716-446655440000", - "threshold": 0.8 + "confidence_threshold": 0.8, + "limit": 100 } ``` ## Response (200 OK) -```json +```json example-response { - "message": "Auto-link completed", - "linked_count": 15, - "unmatched_count": 5, - "links": [ - { - "book_id": "book-123", - "media_item_id": "uuid", - "confidence": 0.95 - } - ] + "auto_linked": null, + "results": null } ``` diff --git a/docs/developer/api/book-matching/bulk_link_books.md b/docs/developer/api/book-matching/bulk_link_books.md index b95f5106..7d9d5591 100644 --- a/docs/developer/api/book-matching/bulk_link_books.md +++ b/docs/developer/api/book-matching/bulk_link_books.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/sync/bulk-link-books +auth: jwt +area: book-matching +--- + + # Bulk Link Books Link multiple books to media items at once. @@ -21,18 +29,18 @@ Each link object contains: ### Example Request -```json +```json example-request { "links": [ { - "device_id": "550e8400-e29b-41d4-a716-446655440000", - "book_id": "book-123", - "media_item_id": "660e8400-e29b-41d4-a716-446655440001" + "unlinked_book_id": "770e8400-e29b-41d4-a716-446655440002", + "media_item_id": "660e8400-e29b-41d4-a716-446655440001", + "confidence_score": 0.95 }, { - "device_id": "550e8400-e29b-41d4-a716-446655440000", - "book_id": "book-456", - "media_item_id": "770e8400-e29b-41d4-a716-446655440002" + "unlinked_book_id": "880e8400-e29b-41d4-a716-446655440003", + "media_item_id": "990e8400-e29b-41d4-a716-446655440004", + "confidence_score": 0.87 } ] } @@ -40,11 +48,12 @@ Each link object contains: ## Response (200 OK) -```json +```json example-response { - "message": "Books linked successfully", - "linked_count": 2, - "failed_count": 0 + "failed": null, + "results": null, + "successful": null, + "total": null } ``` diff --git a/docs/developer/api/book-matching/create_device_file_alias.md b/docs/developer/api/book-matching/create_device_file_alias.md index ce0b2526..3fe75a4c 100644 --- a/docs/developer/api/book-matching/create_device_file_alias.md +++ b/docs/developer/api/book-matching/create_device_file_alias.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/devices/:id/file-aliases +auth: jwt +area: book-matching +--- + + # Create Device File Alias Create a new file alias for a device. @@ -22,7 +30,7 @@ Create a new file alias for a device. ### Example Request -```json +```json example-request { "media_item_id": "550e8400-e29b-41d4-a716-446655440001", "file_name": "book.epub", @@ -32,14 +40,15 @@ Create a new file alias for a device. ## Response (201 Created) -```json +```json example-response { "id": "uuid", "device_id": "device-uuid", "media_item_id": "book-uuid", - "file_name": "book.epub", - "file_hash": "abc123...", - "created_at": "2026-02-08T10:00:00Z" + "confidence_score": null, + "file_path": null, + "file_sha256": null, + "last_seen_at": null } ``` diff --git a/docs/developer/api/book-matching/delete_device_file_alias.md b/docs/developer/api/book-matching/delete_device_file_alias.md index 51514735..bd972af2 100644 --- a/docs/developer/api/book-matching/delete_device_file_alias.md +++ b/docs/developer/api/book-matching/delete_device_file_alias.md @@ -1,3 +1,11 @@ +--- +method: DELETE +path: /api/devices/:id/file-aliases/:aliasId +auth: jwt +area: book-matching +--- + + # Delete Device File Alias Delete a device file alias. @@ -29,6 +37,12 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... File alias deleted successfully. +```json example-response +{ + "message": "File alias deleted successfully" +} +``` + ## Error Responses | Code | Description | diff --git a/docs/developer/api/book-matching/get_device_file_aliases.md b/docs/developer/api/book-matching/get_device_file_aliases.md index 0eb3b2c2..6d48362d 100644 --- a/docs/developer/api/book-matching/get_device_file_aliases.md +++ b/docs/developer/api/book-matching/get_device_file_aliases.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/devices/:id/file-aliases +auth: jwt +area: book-matching +--- + + # Get Device File Aliases Get all file aliases for a specific device. @@ -26,7 +34,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ## Response (200 OK) -```json +```json example-response { "aliases": [ { @@ -38,7 +46,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... "created_at": "2026-02-08T10:00:00Z" } ], - "total": 23 + "total": 23, + "device_id": null } ``` diff --git a/docs/developer/api/book-matching/get_unlinked_book_suggestions.md b/docs/developer/api/book-matching/get_unlinked_book_suggestions.md index f8f64883..8fe1adc4 100644 --- a/docs/developer/api/book-matching/get_unlinked_book_suggestions.md +++ b/docs/developer/api/book-matching/get_unlinked_book_suggestions.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/sync/unlinked-books/:id/suggestions +auth: jwt +area: book-matching +--- + + # Get Unlinked Book Suggestions Get suggested matches for unlinked books on a device. @@ -33,7 +41,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ## Response (200 OK) -```json +```json example-response { "suggestions": [ { @@ -44,18 +52,27 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "media_item_id": "uuid", "title": "Foundation", - "authors": ["Isaac Asimov"], + "authors": [ + "Isaac Asimov" + ], "confidence": 0.95 }, { "media_item_id": "uuid-2", "title": "Foundation and Empire", - "authors": ["Isaac Asimov"], + "authors": [ + "Isaac Asimov" + ], "confidence": 0.75 } ] } - ] + ], + "action": null, + "sha256": null, + "title_from_device": null, + "total_suggestions": null, + "unlinked_book_id": null } ``` diff --git a/docs/developer/api/book-matching/get_unlinked_books.md b/docs/developer/api/book-matching/get_unlinked_books.md index a0571be9..9abc4dbd 100644 --- a/docs/developer/api/book-matching/get_unlinked_books.md +++ b/docs/developer/api/book-matching/get_unlinked_books.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/devices/:deviceId/sync/unlinked-books +auth: jwt +area: book-matching +--- + + # Get Unlinked Books Get all books that haven't been linked to media items yet for a specific device. @@ -33,20 +41,10 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ## Response (200 OK) -```json +```json example-response { - "items": [ - { - "id": "uuid", - "file_name": "book.epub", - "file_path": "/path/to/book.epub", - "size_bytes": 1234567, - "modified_at": "2026-02-08T10:00:00Z" - } - ], "total": 15, - "limit": 10, - "offset": 0 + "unlinked": null } ``` diff --git a/docs/developer/api/book-matching/link_book.md b/docs/developer/api/book-matching/link_book.md index 8a90448b..4a3cd8e6 100644 --- a/docs/developer/api/book-matching/link_book.md +++ b/docs/developer/api/book-matching/link_book.md @@ -1,7 +1,45 @@ +--- +method: POST +path: /api/devices/:deviceId/sync/link-book +auth: jwt +area: book-matching +--- + + # Link Book Link a device book to a Bookhoard media item. Supports bulk linking. +## Example request + +```json example-request +{ + "device_file": { + "file_path": "/books/hobbit.epub", + "sha256": "abc123...", + "title": "The Hobbit" + }, + "media_item_id": "550e8400-e29b-41d4-a716-446655440000", + "confidence_score": 1.0 +} +``` + +## Example response + +```json example-response +{ + "status": "linked", + "device_file_alias": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "media_item_id": "550e8400-e29b-41d4-a716-446655440000", + "device_id": "550e8400-e29b-41d4-a716-446655440000", + "file_path": "/books/hobbit.epub", + "file_sha256": "abc123...", + "confidence_score": 1.0 + } +} +``` + **Endpoint**: `POST /api/sync/bulk-link-books` or `POST /api/sync/auto-link-books` **Auth**: Required **Content-Type**: `application/json` diff --git a/docs/developer/api/book-matching/search_books.md b/docs/developer/api/book-matching/search_books.md index 2b813edc..7b37e11e 100644 --- a/docs/developer/api/book-matching/search_books.md +++ b/docs/developer/api/book-matching/search_books.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/sync/books/query +auth: jwt +area: book-matching +--- + + # Search Books for Matching Query books to find potential matches for linking. @@ -18,7 +26,7 @@ Query books to find potential matches for linking. ### Example Request -```json +```json example-request { "identifiers": ["isbn:978-0345391802", "uuid:abc-123"], "sha256": "a1b2c3d4e5f6abc123...", @@ -30,7 +38,7 @@ Query books to find potential matches for linking. ## Response (200 OK) -```json +```json example-response { "matches": [ { diff --git a/docs/developer/api/book-matching/update_device_file_alias.md b/docs/developer/api/book-matching/update_device_file_alias.md index b9cf6f4e..4b113f7c 100644 --- a/docs/developer/api/book-matching/update_device_file_alias.md +++ b/docs/developer/api/book-matching/update_device_file_alias.md @@ -1,3 +1,11 @@ +--- +method: PUT +path: /api/devices/:id/file-aliases/:aliasId +auth: jwt +area: book-matching +--- + + # Update Device File Alias Update an existing device file alias. @@ -22,7 +30,7 @@ Update an existing device file alias. ### Example Request -```json +```json example-request { "file_name": "updated-book.epub", "file_hash": "newhash123..." @@ -31,14 +39,14 @@ Update an existing device file alias. ## Response (200 OK) -```json +```json example-response { "id": "uuid", - "device_id": "device-uuid", "media_item_id": "book-uuid", - "file_name": "updated-book.epub", - "file_hash": "newhash123...", - "updated_at": "2026-02-08T11:00:00Z" + "confidence_score": null, + "file_path": null, + "file_sha256": null, + "last_seen_at": null } ``` diff --git a/docs/developer/api/collections/add_auto_assign_rule.md b/docs/developer/api/collections/add_auto_assign_rule.md index 280abc4e..5ed8176a 100644 --- a/docs/developer/api/collections/add_auto_assign_rule.md +++ b/docs/developer/api/collections/add_auto_assign_rule.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/collections/:id/rules +auth: jwt +area: collections +--- + + # Add Auto-Assign Rule Add an automatic book assignment rule to a collection. @@ -49,7 +57,7 @@ Add an automatic book assignment rule to a collection. ### Example Request -```json +```json example-request { "field": "genre", "operator": "equals", @@ -61,15 +69,11 @@ Add an automatic book assignment rule to a collection. ## Response (201 Created) -```json +```json example-response { - "id": "rule-uuid-here", - "field": "genre", - "operator": "equals", - "value": "Science Fiction", - "priority": 1, - "enabled": true, - "created_at": "2026-02-01T10:00:00Z" + "collection_id": null, + "is_smart": null, + "rule": null } ``` diff --git a/docs/developer/api/collections/bulk_assign.md b/docs/developer/api/collections/bulk_assign.md index 1c14f7c7..b9a84e2c 100644 --- a/docs/developer/api/collections/bulk_assign.md +++ b/docs/developer/api/collections/bulk_assign.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/collections/:id/books +auth: jwt +area: collections +--- + + # Bulk Assign Books Add multiple books to a collection at once. @@ -20,7 +28,7 @@ Add multiple books to a collection at once. ### Example Request -```json +```json example-request { "book_ids": [ "660e8400-e29b-41d4-a716-446655440000", @@ -40,6 +48,22 @@ Books added to collection successfully. No response body. - This is more efficient than adding books individually - Maximum 1000 books per request +```json example-response +{ + "results": [ + { + "book_id": "660e8400-e29b-41d4-a716-446655440001", + "status": "added" + } + ], + "total": 1 +} +``` + +Each result carries the operation outcome for one book (`added`/`error` +variants come from the per-book loop; malformed ids report their own +status instead of failing the batch). + ## Error Responses | Code | Description | diff --git a/docs/developer/api/collections/create_shelf_mapping.md b/docs/developer/api/collections/create_shelf_mapping.md index 3209547d..43d6b5ba 100644 --- a/docs/developer/api/collections/create_shelf_mapping.md +++ b/docs/developer/api/collections/create_shelf_mapping.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/devices/:id/collections +auth: jwt +area: collections +--- + + # Create Shelf Mapping Map a collection to a device shelf for syncing. @@ -31,7 +39,7 @@ Map a collection to a device shelf for syncing. ### Example Request -```json +```json example-request { "collection_id": "550e8400-e29b-41d4-a716-446655440000", "device_shelf_name": "Sci-Fi", @@ -41,14 +49,14 @@ Map a collection to a device shelf for syncing. ## Response (201 Created) -```json +```json example-response { "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" + "created_at": "2026-02-01T10:00:00Z", + "device_id": null } ``` diff --git a/docs/developer/api/collections/remove_auto_assign_rule.md b/docs/developer/api/collections/remove_auto_assign_rule.md index fc4e5a9a..f9b89887 100644 --- a/docs/developer/api/collections/remove_auto_assign_rule.md +++ b/docs/developer/api/collections/remove_auto_assign_rule.md @@ -1,3 +1,11 @@ +--- +method: DELETE +path: /api/collections/:id/rules/:ruleId +auth: jwt +area: collections +--- + + # Remove Auto-Assign Rule Remove an automatic book assignment rule from a collection. @@ -17,6 +25,16 @@ Remove an automatic book assignment rule from a collection. Rule deleted successfully. No response body. +```json example-response +{ + "collection_id": "550e8400-e29b-41d4-a716-446655440000", + "is_smart": true +} +``` + +`is_smart` reflects the collection state AFTER removal — removing the +last rule converts a smart collection back to static. + ## Error Responses | Code | Description | diff --git a/docs/developer/api/collections/test_rule.md b/docs/developer/api/collections/test_rule.md index 24a96996..9b55e02f 100644 --- a/docs/developer/api/collections/test_rule.md +++ b/docs/developer/api/collections/test_rule.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/collections/test-rules +auth: jwt +area: collections +--- + + # Test Rule Test which books would match given rules without saving. @@ -22,7 +30,7 @@ Test which books would match given rules without saving. ### Example Request -```json +```json example-request { "rules": [ { @@ -41,7 +49,7 @@ Test which books would match given rules without saving. ## Response (200 OK) -```json +```json example-response { "matches": [ { diff --git a/internal/docs/schema.go b/internal/docs/schema.go index b3236f73..7e85bfb6 100644 --- a/internal/docs/schema.go +++ b/internal/docs/schema.go @@ -770,4 +770,94 @@ var Schemas = map[string]Schema{ "GET /opds/devices/:deviceId/catalog": {}, "GET /opds/devices/:deviceId/download/:bookId": {}, "GET /opds/devices/:deviceId/cover/:bookId": {}, + + // --- Beam batch 10: book-matching + collections remainder --- + "POST /api/sync/books/query": { + // Plugin/sync-protocol variant of GET /api/books/match (body, not + // query params; unfiltered legacy path). + Request: services.BookQueryRequest{}, + Response: services.BookQueryResponse{}, + }, + "POST /api/sync/bulk-link-books": { + Request: handlers.BulkLinkBooksRequest{}, + Response: json.RawMessage(`{ + "results": [], "total": 0, "successful": 0, "failed": 0 + }`), + }, + "POST /api/sync/auto-link-books": { + Request: handlers.AutoLinkBooksRequest{}, + Response: json.RawMessage(`{ + "auto_linked": 0, "results": [] + }`), + }, + "GET /api/devices/:deviceId/sync/unlinked-books": { + Response: json.RawMessage(`{ + "unlinked": [], "total": 0 + }`), + }, + "GET /api/sync/unlinked-books/:id/suggestions": { + Response: json.RawMessage(`{ + "unlinked_book_id": "", "title_from_device": "", "sha256": "", + "suggestions": [], "total_suggestions": 0, "action": "" + }`), + }, + "GET /api/devices/:id/file-aliases": { + Response: json.RawMessage(`{ + "device_id": "", "aliases": [], "total": 0 + }`), + }, + "POST /api/devices/:id/file-aliases": { + // Inline request struct in handler. + Request: json.RawMessage(`{ + "media_item_id": "", "file_path": "", "file_sha256": "", + "confidence_score": 0 + }`), + Response: json.RawMessage(`{ + "id": "", "media_item_id": "", "device_id": "", "file_path": "", + "file_sha256": "", "confidence_score": 0, "last_seen_at": "" + }`), + }, + "PUT /api/devices/:id/file-aliases/:aliasId": { + // Partial update — pointer fields. + Request: json.RawMessage(`{ + "media_item_id": "", "file_sha256": "", "confidence_score": 0 + }`), + Response: json.RawMessage(`{ + "id": "", "media_item_id": "", "file_path": "", + "file_sha256": "", "confidence_score": 0, "last_seen_at": "" + }`), + }, + "DELETE /api/devices/:id/file-aliases/:aliasId": { + Response: json.RawMessage(`{"message": ""}`), + }, + "POST /api/collections/:id/rules": { + // First rule converts a static collection to smart. + Request: handlers.AddRuleRequest{}, + Response: json.RawMessage(`{ + "collection_id": "", "rule": {}, "is_smart": false + }`), + }, + "DELETE /api/collections/:id/rules/:ruleId": { + Response: json.RawMessage(`{ + "collection_id": "", "is_smart": false + }`), + }, + "POST /api/collections/test-rules": { + Request: handlers.TestRulesRequest{}, + Response: json.RawMessage(`{"matches": [], "total": 0}`), + }, + "POST /api/collections/:id/books": { + // Bulk add: {operations: [{collection_id, book_ids:[...]}]}. + Request: json.RawMessage(`{ + "operations": [{"collection_id": "", "book_ids": []}] + }`), + Response: json.RawMessage(`{"results": [], "total": 0}`), + }, + "POST /api/devices/:id/collections": { + Request: handlers.CreateDeviceMappingRequest{}, + Response: json.RawMessage(`{ + "id": "", "collection_id": "", "device_id": "", + "device_shelf_name": "", "sync_direction": "", "created_at": "" + }`), + }, }