feat(sync): unlinked-books queue — ingest, page route, dashboard banner
Release / build-and-push (push) Successful in 3m7s
Release / build-and-push (push) Successful in 3m7s
The identification queue for device files the sync cannot match is now live end-to-end. Previously the whole feature was dark: unmatched device files were silently dropped (losing their reading progress), the queue table sat empty by construction, and the page template had no route. - Ingest: the KOReader sync loop, on an unresolvable file, upserts unlinked_books (one row per device + content id; re-syncs bump last_seen_at and refresh device-reported metadata) instead of dropping the file. Schema gains the (device_id, content_id) unique index the upsert needs. - Page: GET /unlinked-books routes the existing page template, listing the caller's own unresolved rows (ListUnresolvedUnlinkedBooksForUser — devices.user_id scoping; no cross-user leakage). - Dashboard banner: a warning card above the shelves, rendered only when the caller's queue is non-empty (hidden-unless-needed), linking to the page. - Resolve flow unchanged and already gated: MatchingHandler.LinkBook verifies device ownership and target-item visibility (403s) and creates the device file alias, so the next sync matches by path. SyncHandler.LinkBook and Handler.LinkBook are unrouted dead handlers — triage candidates, not part of the live flow.
This commit is contained in:
1 parent
9e831085ef
commit
d74db03cf3
10 files changed
+823
-454
No files matched your search
@@ -451,6 +451,10 @@ func (h *KOReaderHandler) SyncProgress(c *echo.Context) error {
|
||||
for _, book := range req.Books {
|
||||
mediaItemID, _ := h.resolveBookToMediaItem(c, device.ID, pgUserID, book)
|
||||
if !mediaItemID.Valid {
|
||||
// Unidentified file: queue it for manual identification
|
||||
// (unlinked-books page) instead of silently dropping the
|
||||
// file's progress. One row per (device, content id).
|
||||
h.queueUnlinkedBook(c, device.ID, book)
|
||||
bookResults = append(bookResults, KOReaderBookSyncResult{
|
||||
SHA256: book.SHA256,
|
||||
Synced: false,
|
||||
@@ -498,9 +502,47 @@ func (h *KOReaderHandler) SyncProgress(c *echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *KOReaderHandler) resolveBookToMediaItem(c *echo.Context, deviceID pgtype.UUID, userID pgtype.UUID, book KOReaderBookProgress) (pgtype.UUID, float64) {
|
||||
// queueUnlinkedBook records a device file that could not be identified, so
|
||||
// the user can resolve it on the unlinked-books page. One row per
|
||||
// (device, content id); re-syncs refresh the row instead of duplicating it.
|
||||
// Best-effort: queueing failures never fail the sync response.
|
||||
func (h *KOReaderHandler) queueUnlinkedBook(c *echo.Context, deviceID pgtype.UUID, book KOReaderBookProgress) {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
contentID := book.UUID
|
||||
if contentID == "" {
|
||||
contentID = book.SHA256
|
||||
}
|
||||
if contentID == "" {
|
||||
// Nothing stable to identify the file by — it could never resolve.
|
||||
return
|
||||
}
|
||||
|
||||
var title pgtype.Text
|
||||
if book.Title != "" {
|
||||
title = pgtype.Text{String: book.Title, Valid: true}
|
||||
}
|
||||
var author pgtype.Text
|
||||
if len(book.Authors) > 0 && book.Authors[0] != "" {
|
||||
author = pgtype.Text{String: book.Authors[0], Valid: true}
|
||||
}
|
||||
var filePath pgtype.Text
|
||||
if book.FilePath != "" {
|
||||
filePath = pgtype.Text{String: book.FilePath, Valid: true}
|
||||
}
|
||||
|
||||
_, _ = h.db.UpsertUnlinkedBook(ctx, database.UpsertUnlinkedBookParams{
|
||||
DeviceID: deviceID,
|
||||
ContentID: contentID,
|
||||
FilePath: filePath,
|
||||
Title: title,
|
||||
Author: author,
|
||||
ConfidenceScore: pgtype.Float8{Float64: 0, Valid: true},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *KOReaderHandler) resolveBookToMediaItem(c *echo.Context, deviceID pgtype.UUID, userID pgtype.UUID, book KOReaderBookProgress) (pgtype.UUID, float64) { ctx := c.Request().Context()
|
||||
|
||||
// Priority 1: UUID provided (highest confidence - 1.0)
|
||||
if book.UUID != "" {
|
||||
mediaUUID, err := uuid.Parse(book.UUID)
|
||||
|
||||
Reference in new issue
Block a user