Files
bookhoard/docs/developer/api/admin/hash-conflicts.md
T
John O'Keefe a8f8c4def9 docs(ui): cookie-rendered sidebar state, data-collapsed CSS, working overview links + link-checker gate
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.
2026-10-24 20:20:07 -04:00

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, missing keep_uuid, or keep_uuid not in the group
  • 404 Not Found — conflict doesn't exist
  • 409 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.