diff --git a/docs/developer/api/admin/hash-conflicts.md b/docs/developer/api/admin/hash-conflicts.md new file mode 100644 index 0000000..102942d --- /dev/null +++ b/docs/developer/api/admin/hash-conflicts.md @@ -0,0 +1,111 @@ +# Hash Conflicts API + +## Overview + +When Bookhoard hashes your library (on import, rescan, or the startup backfill), two media items in the same library with the same `file_sha256` indicate duplicate content. Each duplicate group is recorded as a **hash conflict** and exposed here for an explicit keep/merge decision. Conflicts are also surfaced in the admin UI's Hash Conflicts page. + +**Authentication**: Admin JWT token required +**Content-Type**: `application/json` (resolve also accepts form-encoded bodies for htmx) + +--- + +## Endpoints + +### List Hash Conflicts + +List all pending conflict groups, each with its member items and per-item usage counts (reading progress, highlights, bookmarks, notes, collections) to help decide which copy to keep. + +**Endpoint**: `GET /api/admin/hash-conflicts` + +**Response**: **200 OK** + +```json +{ + "conflicts": [ + { + "id": "conflict-uuid", + "library_id": "library-uuid", + "library_name": "Ebooks", + "sha256": "abc123...", + "created_at": "2026-08-14T12:00:00Z", + "items": [ + { + "id": "media-item-uuid", + "title": "The Hobbit", + "author": "J. R. R. Tolkien", + "file_path": "/books/hobbit.epub", + "file_size": 1048576, + "created_at": "2026-01-01T00:00:00Z", + "progress_count": 2, + "highlight_count": 12, + "bookmark_count": 3, + "note_count": 1, + "collection_count": 2 + } + ] + } + ], + "total": 1 +} +``` + +**Example**: + +```bash +curl -X GET https://bookhoard.example.com/api/admin/hash-conflicts \ + -H "Authorization: Bearer " +``` + +--- + +### Resolve Hash Conflict + +Resolve one conflict group. + +**Endpoint**: `POST /api/admin/hash-conflicts/{id}/resolve` + +**Request Body** (JSON or form-encoded): + +```json +{ + "action": "keep", + "keep_uuid": "media-item-uuid-to-keep" +} +``` + +| Field | Type | Required | Description | +| ----------- | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `action` | string | Yes | `keep_all` — both copies are intentional; dismiss the conflict. `keep` — keep `keep_uuid` and delete the other copies. | +| `keep_uuid` | string | for `action=keep` | The media item UUID to keep. Must belong to this conflict group. With `keep`, every other copy's child rows (progress, highlights, bookmarks, notes, collections, …) are merged into the kept item before the losers are deleted. | + +**Responses**: + +- `200 OK` — resolved (body is an HTML confirmation snippet for the admin UI page) +- `400 Bad Request` — invalid conflict ID, missing `keep_uuid`, or `keep_uuid` not in the group +- `404 Not Found` — conflict doesn't exist +- `409 Conflict` — conflict already resolved + +**Example**: + +```bash +curl -X POST https://bookhoard.example.com/api/admin/hash-conflicts//resolve \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"action": "keep", "keep_uuid": "media-item-uuid"}' +``` + +--- + +## When Conflicts Are Created + +- **Startup backfill**: items imported before hashing existed are hashed automatically ~30s after startup; duplicates discovered land here. +- **Rescan**: hashes are recomputed and content duplicates are flagged. + +Files on disk are never deleted — resolution only affects database rows. + +--- + +## Related Endpoints + +- [System Settings API](../system/settings.md) — scanning configuration +- [Scanner API](../scanner/) — triggering scans and watch mode