feat(sync): propagate KOReader annotation deletions + history API

KOReader push (processBookAnnotations) accepts deleted_highlights and
deleted_bookmarks arrays of dedup keys and tombstones the matching rows,
after the upserts so a key present in both lists resolves to 'deleted'
(the newer intent). Deletions remain soft: rows stay restorable from the
history and echo to other devices as tombstones on their next pull. A
stale device replay of the annotation cannot resurrect the tombstone —
device pushes carry no modification timestamp, so the save loses to the
delete. Absence from these arrays is never a delete, keeping category
toggles safe.

New annotation-history endpoints (annotation_history.go, media.go):
  GET    /api/media-items/:id/annotations/deleted
  POST   /api/media-items/:id/annotations/:annotationId/restore
  DELETE /api/media-items/:id/annotations/:annotationId
All scoped to the authenticated user and the route's book; the DELETE is
the permanent purge (annotation_type required in query or body).

MediaDetail gains DeletedAnnotations, populated by the book page route
via the shared DeletedAnnotationsForBook builder, so the server-rendered
history ships with the page instead of requiring a client round-trip.

Binding tests cover the plugin's exact wire shape and the legacy
plugin case (arrays omitted -> empty).
This commit is contained in:
2026-08-22 13:16:43 -04:00
parent d4c52e9a6a
commit 1f5c5a28bd
6 changed files with 291 additions and 25 deletions
+4
View File
@@ -18,4 +18,8 @@ type MediaDetail struct {
// Computed counts
NotesCount int `json:"notes_count"`
HighlightsCount int `json:"highlights_count"`
// Deleted-annotation history (tombstoned rows, newest first) — the book
// page's "recently deleted" list with restore/permanent-delete actions.
DeletedAnnotations []DeletedAnnotationResponse `json:"deleted_annotations"`
}