fix(metadata): seed age/content rating into Fetch Metadata modal (bookshelf-ry66) #883
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-ry66"
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
AgeRating *int32andContentRating stringtoCandidateResultDTO and populates them from the providerMetadatastruct inmetadataToCandidateResult.metadata_fetch_controller.jsto render compare rows for both fields (with numeric-safedisplayValuehandling forage_rating: 0= All Ages, andage_ratingsent as a JSON number in the save POST body).rating.name→ canonicalAgeRating + ContentRating(unchanged); ComicVine provides neither (unchanged). The persist layer already wired these tobook_metadata.age_rating/content_rating.Root cause fixed
The candidate-to-modal seed step (
metadataToCandidateResultat metadata_service.go:259) copiedRatingbut omittedm.AgeRating/m.ContentRating. All downstream layers (form parser, service, store, SQL) already handled them correctly.Test plan
metadata_candidates_test.gocovering age rating (13, 0=All Ages), content rating, and nil/missing cases. 100% coverage maintained (make coveragepasses).metadata_fetch_controller.test.jscovering row rendering, All Ages edge case, copy button, and save POST body shape. All 3304 JS tests pass.Closes bead bookshelf-ry66 on merge.
CandidateResult now carries AgeRating (*int32) and ContentRating (string), populated in metadataToCandidateResult from the provider Metadata struct. Metron maps rating.name → canonical AgeRating + ContentRating via mapRating; ComicVine provides neither (unchanged). The fetch-metadata compare modal (metadata_fetch_controller.js) now renders age_rating and content_rating compare rows alongside the other standard fields. age_rating 0 ("All Ages") is treated as a valid non-empty value in displayValue. age_rating is sent as a JSON number (added to the numeric field list in _saveFromModal) so the server decodes it into SaveMetadataRequest.AgeRating (*int32) correctly. content_rating is sent as a string. Both fields map to existing persist paths (book_metadata.age_rating / book_metadata.content_rating) which were already wired end-to-end; only the candidate→modal seed step was missing. Tests: Go black-box (FetchCandidates age/content rating mapping), JS Vitest (compare row rendering, copy, save POST body). 100% coverage maintained. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
Security Review — PR #883 (bookshelf-ry66)
Scope: age/content rating carried into
CandidateResult→ compare modal → existing save path.Checks performed:
style=Findings
No security issues found in this diff.
Input validation — PASS. Both
age_ratingandcontent_ratingare validated against their canonical sets ininternal/books/metadata_service.go:374–387(IsCanonicalAgeRating/IsCanonicalContentRating) before any write, for both the JSON path and the form path. The form path additionally range-checks the integer atmetadata_handler.go:311–321before the canonical gate fires. A client POSTing an arbitrary integer (e.g. 99) or an arbitrary string content rating is rejected withErrValidation. The "stored-value pass-through" allowance (lines 375–376, 383–384) is loaded server-side and cannot be spoofed from the request body.Ownership — PASS.
SaveMetadataHandlercallscheckBookAccess(r.Context(), userIDFromRequest(r), id)at line 197 before decoding the body.userIDFromRequestis session-derived. The existing gate is unchanged and covers this new field path.XSS — PASS. Provider-supplied rating values flow through
displayValue()→makeEl({text: …})→node.textContent. The controller's line 9 states this policy explicitly. No newinnerHTMLsink is introduced in the diff.innerHTML = ""clears (line 126, 671, etc.) are pre-existing and carryeslint-disable no-unsanitized/propertywhere needed.Multi-user scoping — PASS.
age_ratingandcontent_ratingare columns onbook_metadata(book-scoped, not per-user). This PR introduces no per-user data path requiringuser_idscoping.Architecture boundary — PASS.
internal/books/metadata_service.goimports onlymetadata,middleware, standard library. No workflow engine import.Secrets / PII — PASS. Log lines in this diff emit only
book_idandtrace_id.CSP / inline style= — PASS. No
style=attributes added.JS numeric coercion edge case — noted, not a finding. The JS save path uses
Number(v)which accepts floats (e.g."13.5"). If a user manually edits the field to a float, Go'sjson.Unmarshalinto*int32will return a decode error (fractional JSON numbers cannot be decoded into integer types), surfacing as a 400. Canonical validation at the service layer is the authoritative gate regardless.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Code Review — PR #883 (bookshelf-ry66)
Diff reviewed: origin/main...origin/bd-bookshelf-ry66 (head
87e9ea8eb5)Files: internal/books/dto.go, internal/books/metadata_service.go, internal/books/metadata_candidates_test.go, static/js/controllers/metadata_fetch_controller.js, static/js/test/metadata_fetch_controller.test.js
Phase 0: DEMO Verification
No DEMO block in the bead completion comment. CI is confirmed green via Forgejo commit-status API (state: success) and PR is mergeable. Per the review task the CI green state serves as functional verification; findings below are diff-based.
Phase 1: Spec Compliance
age_rating 0 (All Ages) on the Go side - PASS.
age_rating 0 on the JS side - PASS.
Locks - PASS. SQL upsert in metadata.sql.go:120 uses IF(COALESCE(age_rating_locked, 0) = 0 AND COALESCE(all_fields_locked, 0) = 0, VALUES(age_rating), age_rating). Lock enforcement at DB level, unchanged.
Canonical validation - PASS. Service rejects non-canonical age_rating values (metadata_service.go:374-379). Go json.Unmarshal into *int32 rejects floats at decode time. IsCanonicalContentRating gate at line 382 unmodified.
Serialisation field names - PASS. JS sends age_rating (number) and content_rating (string), matching SaveMetadataRequest fields.
Scope vs PR 882 (7mzg) - PASS. This PR adds to fieldLabel/_editorFieldID/displayValue/_saveFromModal. It does NOT touch buildComicCompareRow, _comicEditorFieldID, comicFieldLabel, or comicFieldOrder. Non-conflicting sections; clean rebase expected.
Bulk enrich path - PASS. No changes to persistInTxWithComic or wfengine bulk enrichment activities. Bulk path writes AgeRating/ContentRating via metadataToUpsertParams unchanged.
Black-box tests - PASS. metadata_candidates_test.go declares package books_test. No unexported symbols referenced.
CSP / inline style= - PASS. No new style= attributes in the diff.
Phase 2: Code Quality Findings
[MINOR] static/js/test/metadata_fetch_controller.test.js — missing save-path test for age_rating=0
The JS test suite tests copy-and-save with age_rating 13 but never exercises the All Ages path (age_rating 0) through to the POST body. The code is correct (v !== "" passes for "0", Number("0") = 0), and coverage is maintained because the same branch handles both values. But age_rating 0 is the critical case motivating this fix; a future regression that special-cases 0 in the save path would go undetected. Suggested addition: open modal with METRON_CANDIDATE_ALL_AGES, copy age_rating, save, assert capturedBody.age_rating === 0 (number, not string, not absent).
[MINOR] static/js/controllers/metadata_fetch_controller.js:308-315 — compare row shows raw "0" not "All Ages" in fetched column
displayValue("age_rating", {age_rating: 0}) returns "0". The modal fetched column shows 0 while the main form select shows the label "All Ages". Not a correctness bug — the value copies and saves correctly — but potentially confusing to users. Consider mapping numeric buckets to labels inside displayValue when key === "age_rating".
REVIEW VERDICT: 0 blocker, 0 major, 2 minor
Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
e4e565804ada3ad9fb42Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
zombor referenced this pull request2026-07-03 18:15:31 +00:00