docs(api): document hash-conflict resolution endpoints
Cover the admin API added in 03cb4c7 for duplicate-content decisions:
- GET /api/admin/hash-conflicts — pending conflict groups with member
items and per-item usage counts (progress, highlights, bookmarks,
notes, collections)
- POST /api/admin/hash-conflicts/:id/resolve — action=keep (merge child
rows into keep_uuid, delete losers) vs action=keep_all (dismiss);
JSON and form-encoded bodies, error codes including 409 for already
resolved
- When conflicts are created (startup backfill, rescans) and the
guarantee that files on disk are never deleted
This commit is contained in:
@@ -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 <admin_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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/<id>/resolve \
|
||||||
|
-H "Authorization: Bearer <admin_token>" \
|
||||||
|
-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
|
||||||
Reference in New Issue
Block a user