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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user