feat(backend): favorite + latest-chapter fields, conditional updated_at

Adds favorite, latest_chapter and latest_chapter_num to the bookmark record,
with an idempotent ALTER TABLE migration so the already-deployed database
picks them up.

updated_at now moves only when a bookmark is new or last_chapter_num changes.
Clients order their list by updated_at, so favoriting a series or recording a
newly published chapter must not disturb that order. Upsert consequently
returns the row as stored and the handler echoes that rather than the request
payload, since the candidate timestamp it sends is often discarded.

Scanning also tolerates NULL in the optional columns, which a database created
before this code can legitimately contain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 12:06:44 +07:00
parent 053355d0c6
commit 01301805fb
3 changed files with 422 additions and 43 deletions
+8 -3
View File
@@ -60,14 +60,19 @@ func (h *bookmarkHandler) put(w http.ResponseWriter, r *http.Request) {
}
}
}
b.UpdatedAt = time.Now().UnixMilli() // server-assigned, ignore client value
// Candidate timestamp, not a decision: Upsert keeps the stored one unless
// reading progress actually moved. Any client value is ignored.
b.UpdatedAt = time.Now().UnixMilli()
if err := h.store.Upsert(b); err != nil {
stored, err := h.store.Upsert(b)
if err != nil {
log.Printf("upsert: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, b)
// Echo the stored row: clients adopt this as their cached copy, so it must
// carry the authoritative updated_at rather than the candidate above.
writeJSON(w, http.StatusOK, stored)
}
// delete removes one bookmark. DELETE /bookmarks/{key}