diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 6049bb24..e9d73033 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -742,6 +742,45 @@ func registerFrontendRoutes(cfg *Config) { // itself here (htmx) whenever the library switcher dispatches // week-refresh, carrying the current #library-select value. Same // resolution rules as the dashboard page (family-lens safe). + // Unlinked-book match results partial: the match cascade for one device + // file, rendered server-side and scoped to the requester's visible + // libraries (absent-from-results) — rendering-ownership: templ renders, + // JS only swaps. NOTE: nothing routes to the unlinked-books page yet; + // this endpoint is ready for whichever UI wires it up. + frontendProtected.GET("/unlinked-books/matches", func(c *echo.Context) error { + user, ok := c.Get("user").(database.Users) + if !ok { + return c.HTML(http.StatusUnauthorized, "
Unauthorized
") + } + userUUID, uuidErr := uuid.Parse(user.ID.String()) + if uuidErr != nil { + return c.HTML(http.StatusUnauthorized, "
Unauthorized
") + } + set, _, setErr := cfg.LibraryService.VisibleLibrarySet(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) + if setErr != nil { + return c.HTML(http.StatusInternalServerError, "
Failed to resolve visible libraries
") + } + visibleIDs := make([]string, 0, len(set)) + for id := range set { + visibleIDs = append(visibleIDs, id) + } + req := &services.BookQueryRequest{ + SHA256: c.QueryParam("sha256"), + Title: c.QueryParam("title"), + VisibleLibraryIDs: visibleIDs, + } + resp, matchErr := services.NewBookMatchingService(cfg.Queries).QueryBooks(c.Request().Context(), req) + if matchErr != nil { + return c.HTML(http.StatusInternalServerError, "
Failed to search
") + } + + var buf bytes.Buffer + if renderErr := templates.UnlinkedMatchCards(c.QueryParam("progress_id"), resp.Matches).Render(c.Request().Context(), &buf); renderErr != nil { + return renderErr + } + return c.HTML(http.StatusOK, buf.String()) + }) + frontendProtected.GET("/dashboard/week-widget", func(c *echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { diff --git a/internal/services/book_matching.go b/internal/services/book_matching.go index f457f250..10909fb9 100644 --- a/internal/services/book_matching.go +++ b/internal/services/book_matching.go @@ -6,6 +6,8 @@ import ( "fmt" "math" + "bookhoard/internal/utils" + "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) @@ -16,6 +18,10 @@ type BookMatch struct { BookhoardUUID uuid.UUID `json:"bookhoard_uuid"` Confidence float64 `json:"confidence"` MatchMethod string `json:"match_method"` + // Display fields, filled during the family-lens pass in QueryBooks. + Title string `json:"title"` + Author string `json:"author"` + CoverImagePath string `json:"cover_image_path"` } // BookQueryRequest represents a book query from a device @@ -115,23 +121,30 @@ func (s *BookMatchingService) QueryBooks(ctx context.Context, req *BookQueryRequ // matches (absent-from-results). Runs before determineAction so the // suggested action reflects the filtered set. nil (unset) keeps // legacy callers unfiltered. - if req.VisibleLibraryIDs != nil { - allowed := make(map[string]bool, len(req.VisibleLibraryIDs)) - for _, id := range req.VisibleLibraryIDs { - allowed[id] = true - } - filtered := matches[:0] - for _, m := range matches { - item, err := s.db.GetMediaItem(ctx, pgtype.UUID{Bytes: m.MediaItemID, Valid: true}) - if err != nil || !item.LibraryID.Valid { - continue // vanished between cascade and filter, or unscoped — drop - } - if allowed[uuid.UUID(item.LibraryID.Bytes).String()] { - filtered = append(filtered, m) - } - } - matches = filtered + // Enrich + scope in one pass over the cascade hits: display fields come + // from the resolved item, and when scoped the family-lens filter drops + // matches from libraries the requester cannot see + // (absent-from-results). + scoped := req.VisibleLibraryIDs != nil + allowed := make(map[string]bool, len(req.VisibleLibraryIDs)) + for _, id := range req.VisibleLibraryIDs { + allowed[id] = true } + filtered := matches[:0] + for _, m := range matches { + item, err := s.db.GetMediaItem(ctx, pgtype.UUID{Bytes: m.MediaItemID, Valid: true}) + if err != nil || !item.LibraryID.Valid { + continue // vanished between cascade and filter — drop + } + m.Title = item.Title + m.Author = utils.PgTextString(item.Author) + m.CoverImagePath = utils.PgTextString(item.CoverImagePath) + if scoped && !allowed[uuid.UUID(item.LibraryID.Bytes).String()] { + continue + } + filtered = append(filtered, m) + } + matches = filtered // Determine action action := s.determineAction(matches) diff --git a/templates/unlinked_books.templ b/templates/unlinked_books.templ index 1ed1611e..7b9742cc 100644 --- a/templates/unlinked_books.templ +++ b/templates/unlinked_books.templ @@ -1,5 +1,53 @@ package templates +import ( + "fmt" + + "bookhoard/internal/services" +) + + +// UnlinkedMatchCards renders the match candidates for one device file's +// "find matches" reveal — the /unlinked-books/matches partial response. +// Cards are clicked through the delegated data-action handler (rendering +// ownership: templ renders, JS only swaps and handles the click). +templ UnlinkedMatchCards(progressId string, matches []services.BookMatch) { + if len(matches) == 0 { +

No matches found. Try manual linking.

+ } else { + for _, match := range matches { +
+
+ Cover +
+
{ match.Title }
+ if match.Author != "" { +

by { match.Author }

+ } +
+ + { fmt.Sprintf("%.0f%% confidence", match.Confidence*100) } + + { match.MatchMethod } +
+
+
+
+ } + } +} + templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) { diff --git a/templates/unlinked_books_templ.go b/templates/unlinked_books_templ.go index 1e795ddc..65d1aa69 100644 --- a/templates/unlinked_books_templ.go +++ b/templates/unlinked_books_templ.go @@ -8,7 +8,17 @@ package templates import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component { +import ( + "fmt" + + "bookhoard/internal/services" +) + +// UnlinkedMatchCards renders the match candidates for one device file's +// "find matches" reveal — the /unlinked-books/matches partial response. +// Cards are clicked through the delegated data-action handler (rendering +// ownership: templ renders, JS only swaps and handles the click). +func UnlinkedMatchCards(progressId string, matches []services.BookMatch) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -29,29 +39,171 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Unlinked Books - Bookhoard") + if len(matches) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

No matches found. Try manual linking.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + for _, match := range matches { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
\"Cover\"
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(match.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 34, Col: 80} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

by ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(match.Author) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 35, Col: 79} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% confidence", match.Confidence*100)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 38, Col: 64} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(match.MatchMethod) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 40, Col: 85} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + return nil + }) +} + +func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Unlinked Books - Bookhoard") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var2 = []any{"theme-" + user.Theme} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + var templ_7745c5c3_Var11 = []any{"theme-" + user.Theme} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" x-data=\"unlinkedBooks\" x-init=\"setupEventDelegation()\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -59,7 +211,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -67,12 +219,12 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

Unlinked Books

Resolve books that couldn't be automatically matched between devices

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

Unlinked Books

Resolve books that couldn't be automatically matched between devices

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(unlinkedBooks) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
0 selected
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Bulk Manual Link") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(unlinkedBooks) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -114,39 +266,39 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

All Books Linked!

There are no unlinked books that need manual resolution.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

All Books Linked!

There are no unlinked books that need manual resolution.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } for _, book := range unlinkedBooks { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" onchange=\"updateSelectedCount()\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -166,46 +318,46 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 77, Col: 111} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 123, Col: 111} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

Device: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

Device: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 79, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 125, Col: 37} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " (") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " (") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 79, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 125, Col: 58} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ")

File Path: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ")

File Path: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -213,101 +365,101 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 88, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 134, Col: 27} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if book.SHA256 != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
SHA-256: SHA-256: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 94, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 140, Col: 84} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
Last Synced: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
Last Synced: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 99, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 145, Col: 71} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if book.ConfidenceScore > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
Auto-Link Confidence: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
Auto-Link Confidence: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore) + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 105, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 151, Col: 35} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "% confidence
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "% confidence
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

Potential Matches

Potential Matches

Click \"Find Potential Matches\" to search

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" class=\"space-y-3\">

Click \"Find Potential Matches\" to search

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/src/unlinked_books.ts b/web/src/unlinked_books.ts index b3848c9e..d127cc34 100644 --- a/web/src/unlinked_books.ts +++ b/web/src/unlinked_books.ts @@ -18,52 +18,23 @@ function searchMatches( matchesList.innerHTML = '

Searching...

'; - const token = getToken(); - const url = sha256 - ? `/api/books/match?sha256=${sha256}` - : `/api/books/match?title=${encodeURIComponent(title)}`; + // Rendering ownership: results are server-rendered markup (scoped to + // the caller's visible libraries); swap in the templ partial instead of + // rebuilding cards from JSON. + const params = new URLSearchParams({ progress_id: progressId }); + if (sha256) params.set("sha256", sha256); + if (title) params.set("title", title); - fetch(url, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .then((response) => response.json()) - .then((result) => { - if (result.matches && result.matches.length > 0) { - matchesList.innerHTML = result.matches - .map( - (match: any) => ` -
-
- Cover -
-
${match.title}
-

by ${match.author || "Unknown"}

-
- - ${Math.round(match.confidence * 100)}% confidence - - ${match.match_method} -
-
-
-
- `, - ) - .join(""); - } else { - matchesList.innerHTML = - '

No matches found. Try manual linking.

'; - } + window.htmx + .ajax("GET", `/unlinked-books/matches?${params.toString()}`, { + target: `#matches-list-${progressId}`, + swap: "innerHTML", }) - .catch((error) => { - console.error("Failed to search", error); - matchesList.innerHTML = - '

Failed to search

'; + .then((ok) => { + if (!ok) { + matchesList.innerHTML = + '

Failed to search

'; + } }); }