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.
3.7 KiB
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
{
"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:
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):
{
"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, missingkeep_uuid, orkeep_uuidnot in the group404 Not Found— conflict doesn't exist409 Conflict— conflict already resolved
Example:
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 — scanning configuration
- Scanner API — triggering scans and watch mode