fix(hardcover): surface hardcover_book_id in form; fix ISBN query returning id=0 (bookshelf-mzvjv) #1076
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-mzvjv"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
isbnSearchQueryomittedidandslugfrombookFields, so every ISBN-matched book hadb.ID=0andb.Slug="". This wrote"0"to bothhardcover_idandhardcover_book_id.persistMatchedProviderIDfor thehardcoverprovider only wroteMatchedCandidateProviderResourceIDtohardcover_id(the slug column), never tohardcover_book_id(the numeric sync column). The form field was also bound tohardcover_id(varchar 512, slug) rather thanhardcover_book_id(varchar 100, numeric).idandslugtobookFields; propagate slug→hardcover_idand numeric→hardcover_book_idthrough candidate result, save request, andpersistMatchedProviderID; bind the form field tohardcover_book_id; add lock infrastructure for the new field.Files changed
internal/metadata/hardcover/provider.go— fix bookFields + booksByIDsQuery; add Slug to bookResult; set ProviderResourceID=slug, HardcoverBookID=numericinternal/books/dto.go— add HardcoverBookID to CandidateResult + SaveMetadataRequest; rename ProviderIDsRequest.HardcoverID→HardcoverBookIDinternal/books/metadata_service.go— propagate HardcoverBookID; switch persistMatchedProviderID to write slug+numeric separatelyinternal/books/metadata_handler.go— form fieldprovider_hardcover_book_id, max len 100internal/books/metadata_store.go— SaveProviderIDs writes only hardcover_book_id; hardcover_book_id in lockableFieldsinternal/db/sqlc/metadata_extra.go— SetHardcoverBookIDLocked SQL helperinternal/books/wire.go— wire SetHardcoverBookIDLockedtemplates/pages/books_show.html— form field bound to HardcoverBookIDstatic/js/controllers/metadata_fetch_controller.js— send matched_candidate_hardcover_book_idscripts/screenshot_hardcover_provider_id/main.go— screenshot harnessTest plan
make test— all unit tests passmake coverage— 100% coverage gate maintainedCloses bead bookshelf-mzvjv on merge.
Root cause: isbnSearchQuery omitted the `id` and `slug` fields from bookFields, so b.ID was always 0 and b.Slug was always empty. booksByIDsQuery had `{id%s` instead of `{%s` so it duplicated `id` when bookFields already contained it. Changes: - hardcover/provider.go: add `id` and `slug` to bookFields; fix booksByIDsQuery to use `{%s` (not `{id%s`); add Slug to bookResult; set ProviderResourceID=slug, HardcoverBookID=numeric-id-string in toMetadata - books/dto.go: add HardcoverBookID to CandidateResult; add MatchedCandidateHardcoverBookID to SaveMetadataRequest; rename ProviderIDsRequest.HardcoverID → HardcoverBookID (the sync column) - books/metadata_service.go: propagate HardcoverBookID through candidate result; switch on provider to write slug → hardcover_id, numeric → hardcover_book_id - books/metadata_handler.go: form reads/writes provider_hardcover_book_id (max 100) - books/metadata_store.go: SaveProviderIDs writes only hardcover_book_id; add hardcover_book_id to lockableFields; wire SetHardcoverBookIDLocked - internal/db/sqlc/metadata_extra.go: add SetHardcoverBookIDLocked SQL helper - books/wire.go: wire SetHardcoverBookIDLocked - templates/pages/books_show.html: form field → hardcover_book_id, label "Hardcover ID" - static/js/controllers/metadata_fetch_controller.js: send matched_candidate_hardcover_book_id - All affected tests updated; new ISBN-path test in provider_test.go Closes bead bookshelf-mzvjv on merge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>UI Screenshot — Hardcover ID field (sync column)
The Hardcover ID field in Provider IDs & Ratings now shows
hardcover_book_id(the numeric sync key Hardcover.app uses), not
hardcover_id(the slug).After a Hardcover match the field shows the real numeric ID instead of
0.UI Review — PR #1076 (bookshelf-mzvjv) — Hardcover ID field rebinding
Screenshot reviewed:
mzvjv_hcid.png— Edit Metadata form, Provider IDs & Ratings section for "The Hobbit".What I see in the rendered screenshot
12345— the correct numerichardcover_book_idvalue.0anywhere in the section. The regression is confirmed gone..btn-lockclass, same size and position)..metadata-rowalongside Google Books ID, using the same.metadata-field metadata-field--mdwidth class. No alignment anomalies.Source cross-check
The diff is a pure rebinding —
HardcoverID→HardcoverBookIDacross the inputname,id,value, and both lock-state attributes. No structural change to surrounding markup.All classes are canonical:
.metadata-field/.metadata-field--md/.metadata-field-label/.metadata-field-control/.metadata-field-input/.btn-lock— all defined instatic/css/main.css(lines 3247–3430).style=attributes intemplates/pages/books_show.html(confirmed by grep).var(--border),var(--fg-muted),var(--space-*)tokens — no hardcoded hex or px values bespoke to this field.Finding
[MINOR] templates/pages/books_show.html:1260-1266 — Pre-existing: dual "PROVIDER IDS & RATINGS" heading when section is open
The
<details>/<summary>row and the inner<fieldset>/<legend>both render the same text. When expanded, the summary ("▼ PROVIDER IDS & RATINGS") and the legend ("PROVIDER IDS & RATINGS") are both visible, creating a visual duplicate. This pattern is pre-existing across all collapsible sections and is not introduced by this PR. No action required in this PR; a follow-up bead could hide the legend with.metadata-section-details[open] > .metadata-section > legend { display: none; }or by removing the legend from the disclosed fieldset.REVIEW VERDICT: 0 blocker, 0 major, 1 minor
Security Review — PR #1076 (bookshelf-mzvjv)
Read-only adversarial security review of the diff (
git diff origin/main...origin/bd-bookshelf-mzvjv). Focus: SQL injection, lock-honoring on the newhardcover_book_idwrite, user-input handling, secrets/PII.1. SQL injection — clean. Every new write is fully parameterized (
?placeholders, values passed asExecContextargs):internal/db/sqlc/metadata_extra.go:235SetHardcoverBookID—SET bm.hardcover_book_id = ?…ExecContext(ctx, query, hardcoverBookID, bookID).internal/db/sqlc/metadata_extra.go:252SetHardcoverBookIDLocked—SET bm.hardcover_book_id_locked = ?, bound value.internal/db/sqlc/metadata_extra.go:149UpdateBookProviderIDs—bm.hardcover_book_id = IF(…, ?, bm.hardcover_book_id), arg order matches columns. No provider- or user-supplied string is ever interpolated.2. Lock honoring — clean. The new
hardcover_book_idwrite honors both guards identically to sibling fields:metadata_extra.go:240-242):WHERE bm.book_id = ? AND COALESCE(bm.all_fields_locked, 0) = 0 AND COALESCE(bm.hardcover_book_id_locked, 0) = 0. CorrectANDconjunction — a locked id (either flag) is a no-op, no clobber.metadata_extra.go:149):IF(COALESCE(bm.all_fields_locked,0)=0 AND COALESCE(bm.hardcover_book_id_locked,0)=0, ?, bm.hardcover_book_id)— self-referential else-branch preserves the locked value.wire.go:376→d.Q.SetHardcoverBookIDLocked(ctx, locked, bookID);metadata_store.goregistershardcover_book_idinlockableFieldsand theToggleFieldLockswitch. No AND/OR inversion.3. User input — clean. The
provider_hardcover_book_idform field is length-bounded to 100 (metadata_handler.go:365providerIDMaxLen), matching thevarchar(100)column; over-length →ErrValidation(setStr,metadata_handler.go:387). The JSONmatched_candidate_hardcover_book_idpath (metadata_service.go:648) is parameterized and only ever carries the provider's numeric id (strconv.FormatInt,provider.go); a malformed over-length client value cannot inject (parameterized) and fails non-fatally if it overflows. No new route/handler and no change to the existing auth gating on the metadata-save endpoint — no new unauthenticated write surface. The template renders the value into avalue="…"attribute viahtml/templateauto-escaping (books_show.html) — no XSS, no inlinestyle=.4. Secrets/PII — clean. The persisted Hardcover slug (
hardcover_id) and numeric id (hardcover_book_id) are public, non-sensitive identifiers. The only new log line (metadata_service.go:633Warn) emitsbook_id,provider,error,trace_id— never the id/slug value. No tokens/keys echoed.REVIEW VERDICT: 0 blocker, 0 major, 0 minor