Three problems, one root cause each: 1. Sidebar jump: the server rendered its own default (current section open, rest collapsed) and restoreDocsNav corrected it after paint — open-then-close. Fix: persistence moved server-side. toggleCollapsible now writes a docs_nav cookie (strictly-necessary UI state, no identifiers, SameSite=Lax); ApplyNavOverrides merges it into the nav before render, so first paint IS the visitor's state. The post-paint restore is deleted. 2. Arrows out of sync / double-click: two visibility mechanisms disagreed. There was NO data-collapsed CSS at all — collapsed sections were only ever hidden by JS after paint, and the arrow rotation came from a different (server) state. Now: style.css hides on [data-collapsed=true] (single source of truth, server-rendered), applyCollapsed sets only the attribute, arrows rotate from the same merged value. Live check: 37/37 collapsibles arrow-consistent. 3. api-reference.md links: all 20+ quick links were directory-style (authentication/ etc.) that 404 on the docs site, plus stale targets (books/, ../collections-api.md, websocket 'coming soon'). Rewritten to real per-file links — all 9 unique targets return 200 live. Same rot fixed in admin/hash-conflicts.md, development.md, android-app.md, create_media_item.md, koreader-protocol.md; docs/index.md redirected from the deleted collections-api monolith (stale 530-line duplicate of the gated api/collections docs — deleted, per the WO2 doctrine). New gate: TestDocsRelativeLinks walks docs/developer/** and fails make test on any relative link that is directory-style or unresolvable — overview links can never silently rot again.
112 lines
3.7 KiB
Markdown
112 lines
3.7 KiB
Markdown
# 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/get_settings.md) — scanning configuration
|
|
- [Scanner API](../scanner/scan_library.md) — triggering scans and watch mode
|