docs(api): deleted-annotation history + KOReader deletion propagation

New media-items/deleted_annotations.md for the list/restore/purge
endpoints; endpoint index updated. The KOReader protocol page documents
deleted_highlights/deleted_bookmarks on the progress push and the
deletion-propagation contract: keys learned only from server pulls,
explicit arrays only (never absence), tombstone convergence via the
metadata fetch, no resurrection from stale replays, and the web history
as the restore path.
This commit is contained in:
2026-08-22 13:16:54 -04:00
parent f70579b4fc
commit b91326b207
3 changed files with 116 additions and 0 deletions
+4
View File
@@ -107,6 +107,10 @@ See [Media Item Operations](media-items/)
- GET /api/media-items/:id/highlights/:highlightId - Get highlight
- PUT /api/media-items/:id/highlights/:highlightId - Update highlight
- DELETE /api/media-items/:id/highlights/:highlightId - Delete highlight
- GET /api/media-items/:id/bookmarks - Get bookmarks
- GET /api/media-items/:id/annotations/deleted - List deleted annotations (history)
- POST /api/media-items/:id/annotations/:annotationId/restore - Restore a deleted annotation
- DELETE /api/media-items/:id/annotations/:annotationId?annotation_type=highlight|note|bookmark - Permanently delete a deleted annotation
- POST /api/media-items - Create media item (admin)
- PUT /api/media-items/:id - Update media item (admin)
- DELETE /api/media-items/:id - Delete media item (admin)
@@ -0,0 +1,84 @@
# Deleted Annotations History
List, restore, or permanently delete tombstoned annotations (highlights,
notes, bookmarks) for a book. Deletions — from the web or propagated from a
synced device — are soft-deleted and retained for the sync retention window
(default 30 days), powering the book page's "Recently deleted" list. A
restore returns the row to the active set on every synced device; a purge
removes it immediately and irreversibly.
All endpoints require user JWT authentication and operate only on the
caller's own annotations.
## List Deleted Annotations
**Endpoint**: `GET /api/media-items/:id/annotations/deleted`
Returns tombstoned annotations for the book, newest deletion first.
### Response (200 OK)
```json
{
"deleted_annotations": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"annotation_type": "highlight",
"display_text": "the chosen text",
"secondary_text": "user note",
"color": "#ffd54f",
"deleted_at": "2026-08-22T15:04:05Z",
"created_at": "2026-08-01T10:00:00Z"
}
],
"total": 1
}
```
| Field | Description |
| --------------- | ------------------------------------------------------ |
| annotation_type | `highlight`, `note`, or `bookmark` |
| display_text | Highlighted text / note content / bookmark title |
| secondary_text | Note text (highlights) or notes field (bookmarks) |
## Restore Deleted Annotation
**Endpoint**: `POST /api/media-items/:id/annotations/:annotationId/restore`
Body (or query param) `annotation_type` must be `highlight`, `note`, or
`bookmark`. Clears the tombstone; the annotation reappears in the active
set and re-syncs to devices on their next pull.
```json
{ "annotation_type": "highlight" }
```
### Response (200 OK)
```json
{ "restored": true }
```
404 when no matching *deleted* annotation exists for this user and book.
## Permanently Delete Annotation
**Endpoint**: `DELETE /api/media-items/:id/annotations/:annotationId?annotation_type=highlight|note|bookmark`
Removes the tombstoned row from the history immediately. Irreversible —
unlike the tombstone itself, which is restorable until the retention window
lapses and the daily maintenance sweep purges it.
### Response (200 OK)
```json
{ "purged": true }
```
## Error Responses
| Code | Description |
| ---- | -------------------------------------------------- |
| 400 | Invalid IDs or missing/unknown `annotation_type` |
| 401 | Not authenticated |
| 404 | No matching deleted annotation |
@@ -33,6 +33,8 @@ KOReader uses a custom JSON-based sync protocol.
| books[].epubcfi | string | No | EPUB CFI location |
| books[].character | integer | No | Character offset |
| books[].bookmarks | array | No | Array of bookmarks/highlights (shape, color mapping, and echo/dedup rules: see [Sync Bookmarks](../koreader/sync_bookmarks.md)) |
| books[].deleted_highlights | array | No | Highlights deleted on the device: `[{ "dedup_key": "..." }]` — keys previously served to this device (see [Deletion propagation](#deletion-propagation)) |
| books[].deleted_bookmarks | array | No | Bookmarks deleted on the device: `[{ "dedup_key": "..." }]` |
\* At least one of `uuid` or `sha256` should be present. The server resolves the
book through the shared `BookResolver` with this priority: `uuid``sha256`
@@ -170,6 +172,32 @@ returned so clients can cache it regardless of how the book was originally
obtained. The library list endpoint (`GET /api/sync/koreader/library`) includes
the same `sha256` field on each book.
## Deletion propagation
The progress push is upsert-only: absence of an annotation from
`highlights`/`notes`/`bookmarks` is **never** interpreted as a delete (a
client with a category disabled must not wipe the server). Deletions are
reported explicitly:
- Devices remember the `dedup_key` of every annotation the server served
them (persisted locally, e.g. KOReader's sidecar `bookhoard_known_keys`).
- When one of those annotations no longer exists locally, the next push
lists its key in `deleted_highlights` / `deleted_bookmarks`.
- The server tombstones the matching rows (`deleted = TRUE`, kept for the
retention window). Tombstones are served back to *other* devices via the
metadata fetch's `deleted_highlights` / `deleted_bookmarks` arrays so the
deletion converges everywhere.
- A stale replay pushing the annotation's content cannot resurrect the
tombstone: device pushes carry no modification timestamp, so the save is
treated as older than the delete.
- Restoring is possible from the web book page's deleted-annotation
history (`GET /api/media-items/:id/annotations/deleted`, restore/purge
endpoints) until the retention window lapses.
Because keys are only learned from server pulls, a device-native annotation
deleted locally is simply never pushed again — it can never be mis-flagged
as a server annotation deletion.
## Book identification
Every client/sync interface (KOReader, Kobo, OPDS, the device-link UI, and any