fix(highlights): web edits update the targeted row instead of minting duplicates
PUT /highlights/:id parsed the row id from the URL and then dropped it: the sync-aware path routed through SaveHighlight's content-derived dedup key, on the assumption that the same text + CFI always resolves to the same key. That assumption breaks in practice — the stored epubcfi_start drifts from foliate's range shape to the converter's point shape after a device echo rewrites the row (bucketPosition cuts at the last colon, so '…/6,/1:367,…' and '…/6/1:367' bucket differently), and the user can edit the selection text. The recomputed key then misses the row being edited and createHighlight mints a second one: the edited row (with note, no device pos0) beside the original — served to KOReader as two highlights, one noted and one not. Editing a selection's text would hit the same trap. SaveHighlightRequest gains an optional HighlightID. When set, the save resolves the row by id (ownership-checked), LWWs against it under its stored dedup key, and never re-derives identity from content. The PUT handler passes the already-parsed id. Device pushes, Kobo, and the sync queue send no id and keep the identity-based flow untouched. applyLWW also stops wiping stored locators on web edits: the web reader sends empty start/end positions (it never had a CRE xpointer), so a note/color edit now keeps the device-native positions and CFIs instead of blanking them — round-trip serve-back for device-created highlights survives web-side edits.
This commit is contained in:
@@ -1656,6 +1656,11 @@ func (mh *MediaHandler) UpdateMediaHighlight(c *echo.Context) error {
|
||||
ChapterReference: req.ChapterReference,
|
||||
Source: "web",
|
||||
ModifiedAt: time.Now(),
|
||||
// The PUT targets this exact row (from the URL): identity must
|
||||
// not be re-derived from content — a device echo has usually
|
||||
// rewritten the stored CFI to point shape, so the computed key
|
||||
// would miss and mint a duplicate beside the edited row.
|
||||
HighlightID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
|
||||
Reference in New Issue
Block a user