Bookdrop merge-order drag-to-reorder Settings UI (bookshelf-z9xy) #770
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-z9xy"
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
PUT /settings/bookdrop-merge-orderhandler that validates provider IDs and persists the chain viaSaveBookdropMergeChainSaveBookdropMergeChainfrom bookshelf-8uas/#764)provider-priorityStimulus controller and.provider-priority-*CSS — no bespoke classesProviderPriorityRowtemplate view type carries ProviderID, human-readable Name, and Priority rankprovider_priority_controller.jsscript tag added to the settings shell page scriptsTest plan
make test— all unit tests passmake coverage— 100% Go coverage maintained;check-coverage: OKnpm run coverage— 100% JS coverage maintained (2699 tests pass)make e2e-policy-check— all e2e Describes are Ordered journey containersjourney_bookdrop_merge_order_test.gocovers: navigate to Settings/Metadata → assert section renders → assert default order → drag reorder via__stimulusAppcontroller → save → assert "Saved!" status → reload → assert persisted orderbookdrop_merge_order_handler_test.gocovers 204 success, empty order validation, malformed JSON, unknown provider, duplicate provider, service errorLoadMetaRatingPriority,LoadMetaBookdropMergeOrder) inloadMetadataTabDataCloses bead bookshelf-z9xy on merge.
SECURITY REVIEW — PR #770 (bookshelf-z9xy)
Reviewed diff:
git diff origin/main...origin/bd-bookshelf-z9xyAdmin gate:
PUT /settings/bookdrop-merge-orderis registered underadminRequired(...)ininternal/settings/routes.go:61. TheadminRequiredwrapper enforcesIsAdminfrom the JWT claims — no admin, 403.CSRF: The global middleware chain in
internal/app/app.goappliesmiddleware.CSRF(...)to all unsafe methods (POST, PUT, PATCH, DELETE) before the mux. The JS controller sendsX-CSRF-Tokenextracted from the<meta name="csrf-token">tag. CSRF is fully covered.Input validation — server side:
bookdrop_merge_order_handler.godecodes the JSON body, rejects an empty order, iterates every element throughisKnownProvider()(closed allowlist of 5 hard-coded IDs), and rejects duplicates. Unknown or crafted IDs are rejected withErrValidationbeforeSaveBookdropMergeOrderis called. A second validation pass also runs insideSaveBookdropMergeChain. Double-validated; no injection surface.Request body cap:
middleware.MaxBytesapplies a 1 MB global cap. With ≤5 known provider IDs, no oversized payload can bypass validation before being truncated.UserID / authorization source: The route is only reachable after the
AuthMiddlewaresets claims;adminRequiredreadsIsAdminfrom the JWT claims, not from any request body or query parameter.XSS — DOM rendering: The JS controller reads provider IDs with
el.dataset.providerId(attribute read) and sets status text viathis.statusTarget.textContent = msg— textContent assignment, not innerHTML. No XSS vector.XSS — template rendering:
templates/pages/settings_shell.htmlrenders{{.ProviderID}}and{{.Name}}via Gohtml/template, which auto-escapes both attribute and text contexts. Provider names come fromproviderDisplayNames(a static in-binary map of 5 entries), not from any user input.Inline style= (CSP): No
style=attributes introduced. New elements use CSS class names only.Secrets: No keys, tokens, or credentials in any new code path.
Audit:
audit.Recordis called on successful save — the settings change is logged.Silent error on load failure:
loadMetadataTabDataswallows the error fromLoadMetaBookdropMergeOrderand omits the section rather than surfacing it. This is an acceptable UX degradation (section hides on DB error) with no security implication.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
UI Review — bookshelf-z9xy (PR #770)
Screenshot gate
No rendered screenshot was posted to this PR. The bead spec (
post-ui-screenshots-in-pr) requires a go-rod browser screenshot of the rendered UI state (e.g. the populated Bookdrop Import Metadata Order list) to be attached to the PR before review. The PR description and bead comments confirm none was posted — theAWAITING REVIEWcomment was filed without it.Source cross-check (what I can verify without pixels)
Canonical-component reuse: the new section correctly reuses the existing
.provider-priority-list/.provider-priority-item/.provider-priority-handle/.provider-priority-name/.provider-priority-rankCSS classes fromstatic/css/main.css(lines 9101–9144), all using--space-*andvar(--*)tokens, no hardcoded hex. Theprovider-priorityStimulus controller is the same one already on main — no bespoke parallel class system introduced. Button uses.btn.btn-primary(canonical). No inlinestyle=anywhere in the diff.Single finding:
[MAJOR] templates/pages/settings_shell.html — no rendered screenshot posted to PR
The
post-ui-screenshots-in-prpolicy requires every PR touchingtemplates/to include a go-rod browser screenshot of the rendered component attached to the PR before review. PR #770 has no attachment — only anAWAITING REVIEWbead comment. The ui-reviewer agent cannot verify spacing, drag-handle clarity, row height, or alignment against the canonical provider-priority reference without a rendered PNG. A UI PR without a rendered screenshot is not review-ready per.claude/rules/review-standard.md(UI PR has no rendered screenshot to review → gate failure).Fix: post a go-rod screenshot of the Settings → Metadata tab showing the populated "Bookdrop Import Metadata Order" drag list in its default (expanded) state as a Forgejo attachment comment on this PR. The review can then complete the visual portion.
REVIEW VERDICT: 0 blocker, 1 major, 0 minor
CODE REVIEW: APPROVED — bookshelf-z9xy
Reviewed diff:
origin/main...origin/bd-bookshelf-z9xy(621 lines, 8 files)CI state (SHA
1c6b6cbc61): green (6/6 checks) | mergeable: TruePhase 0: DEMO Verification
No DEMO block is present (this is a Settings UI bead — the browser e2e journey IS the verification, and CI ran it green). Proceeding per bead policy.
Phase 1: Spec Compliance
All spec requirements are satisfied:
/settings/bookdrop-merge-orderwired withadminRequired—internal/settings/routes.go:60SaveBookdropMergeChain(viaBookdropMergeOrderDeps.SaveBookdropMergeOrder—internal/settings/wire.go:160)isKnownProviderand rejects duplicates —bookdrop_merge_order_handler.go:40–48LoadBookdropMergeChainalways returnsdefaultBookdropMergeChainonsql.ErrNoRowsor empty row —priority.go:71–80; section is guarded by{{if .MetaBookdropMergeOrder}}—settings_shell.html:537provider-prioritycontroller reused (not a bespoke parallel) —settings_shell.html:534–567,provider_priority_controller.jsloaded in<script>blockstyle=attributes in the new template sectionjourney_bookdrop_merge_order_test.go:198–276wire.go,shell_handler.go:156Phase 2: Code Quality
[MINOR]
internal/settings/bookdrop_merge_order_handler_test.go:50-53— two-Expect unit testIt("returns 204")The
It("returns 204")block has two separateExpectcalls (Expect(err).NotTo(HaveOccurred())+Expect(rec.Code).To(...)). Project convention for unit tests is one-Expect-per-It; the canonical fold isExpect(rec.Code, err).To(Equal(http.StatusNoContent)). The siblingIt("persists the order slice")already uses the folded form correctly (Expect(capturedOrder, err)). Suggested fix: collapse the 204 It toExpect(rec.Code, err).To(Equal(http.StatusNoContent))and delete the redundant standaloneExpect(err)line.[MINOR]
internal/settings/bookdrop_merge_order_handler_test.go:135-150—Context("on a successful update — audit.Record is a safe no-op")is redundantThis context exercises the same happy path as
Context("when given a valid reorder request")but with a 1-item order and no new branch. It covers no path that the first context doesn't already exercise, and its comment aboutaudit.Recordbeing a no-op reads as over-testing/overjustification. Could be deleted without coverage loss.[MINOR]
internal/settings/shell_handler.go:337— nil guard onLoadMetaBookdropMergeOrderis inconsistent with how the function is wiredAll call sites in
wire.goalways setLoadMetaBookdropMergeOrder, so theif d.LoadMetaBookdropMergeOrder != nilguard is defensive dead code. Other dep fields inShellDepsthat are always wired (e.g.LoadMetaWeights,LoadMetaFieldMatrix) are not nil-guarded. Minor inconsistency; no correctness impact since the guard is safe-by-default.No BLOCKERs. No MAJORs. Admin-scoping is correct. No CSP violations. No N+1. No flake patterns. Canonical component reuse is clean.
REVIEW VERDICT: 0 blocker, 0 major, 3 minor
Settings → Metadata: Bookdrop Import Metadata Order (rendered screenshot, fix for bookshelf-z9xy)
Shows the populated drag-to-reorder provider list in its default state (Google Books, Open Library, Hardcover).
- Fold two-Expect It("returns 204") into single Expect(rec.Code, err) - Delete redundant Context("audit.Record is a safe no-op") (duplicates valid-reorder happy path) - Remove dead LoadMetaBookdropMergeOrder nil-guard; add stub to stubShellDeps for coverage - Post rendered screenshot of Settings → Metadata merge-order section to PR #770 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>UI Review — bookshelf-z9xy (PR #770)
Screenshot downloaded and rendered. Re-review of prior [MAJOR] (no screenshot posted).
What I see in the rendered screenshot
The "Bookdrop Import Metadata Order" section renders correctly in the Metadata settings tab. Three provider rows (Google Books #1, Open Library #2, Hardcover #3) are visible with the hamburger drag-handle (≡) on the left, provider name in the middle, and ordinal badge (#N) on the right — exactly matching the existing
provider-priority-*component layout used elsewhere in the settings page. The rows are visually consistent with the adjacent sections (same card background, border, spacing). The "Save Import Order" button uses the canonical.btn.btn-primaryclass. No content is clipped, overlapping, or duplicated.Component-reuse check
The new section at
templates/pages/settings_shell.html:539–573uses:.settings-section— canonical settings wrapper.provider-priority-list/.provider-priority-item/.provider-priority-handle/.provider-priority-name/.provider-priority-rank— pre-existing canonical classes defined instatic/css/main.css:9101–9144(NOT introduced by this PR).btn.btn-primary— canonical button class.settings-actions/.settings-save-status— canonical layout/status classesdata-controller="provider-priority"— the existing Stimulus controller, not a hand-rolled oneNo bespoke parallel class system. No
modal-*variants. Nostyle=attributes anywhere in the diff.CSS / token check
static/css/main.csswas not modified by this PR. Allprovider-priority-*CSS definitions usevar(--space-2),var(--space-3),var(--space-4),var(--fg-muted),var(--border),var(--accent),var(--surface)— canonical tokens throughout. Thevar(--surface, #1a1d27)and similar fallback hex values are pre-existing in the file and not introduced here.Screenshot gate
Screenshot is attachment-only (comment ID 9814,
/attachments/eec24923-768c-47d2-986d-3d49f01d0275). No PNG or screenshot script committed to the branch — confirmed bygit diff --name-only(8 files, all.go+ one.html, no.png/.sh).Drag affordance
CSS
cursor: grabis set on.provider-priority-item(line 9116). The ≡ handle is visible in the screenshot. CSP-safe — affordance is pure CSS, nostyle=attribute.No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
CODE REVIEW (re-review): APPROVED — bookshelf-z9xy
Reviewer: adversarial code-review agent (second pass after minor fixes)
Diff:
origin/main...origin/bd-bookshelf-z9xyHEAD67443cebPhase 0: 3-minor verification
(1) Folded two-Expect It("returns 204")
internal/settings/bookdrop_merge_order_handler_test.go:50Confirmed:
Expect(rec.Code, err).To(Equal(http.StatusNoContent))— single folded Expect. Prior separateExpect(err).NotTo(HaveOccurred())is gone.(2) Deleted redundant audit no-op Context
internal/settings/bookdrop_merge_order_handler_test.go(line ~132 in pre-fix)Confirmed:
Context("on a successful update — audit.Record is a safe no-op without middleware")block deleted. 11 lines removed in commit67443ceb. Remaining test count still covers all meaningful branches.(3) Dead
if d.LoadMetaBookdropMergeOrder != nilnil-guard removedinternal/settings/shell_handler.go:337Confirmed: guard removed. Direct call
if chain, bmErr := d.LoadMetaBookdropMergeOrder(ctx); bmErr == nil {. Stub added tostubShellDepsatinternal/settings/shell_handler_test.go:75— the always-expected func pointer is now always non-nil, matching the pattern of all other non-optional deps.Phase 1: Spec Compliance
routes.go:61—adminRequired(eh.Wrap(...))confirmed.isKnownProvidervalidation:bookdrop_merge_order_handler.go:39— confirmed.bookdrop_merge_order_handler.go:43— confirmed.btn btn-primary,settings-section,settings-section-title):templates/pages/settings_shell.html:537-576— confirmed.e2e/browser/journey_bookdrop_merge_order_test.go— confirmedOrdered,BeforeAllfresh DB+browser, 5Itsteps covering render → default order → draggable → drag+save → persist-after-reload.git diff --name-onlyshows 8 files, none.png/.sh. The upload helper is embedded in the e2e test file (same pattern asjourney_settings_shell_test.go:37).provider_priority_controller.jsand its CSS already existed inmain; no new JS/CSS file additions.Phase 2: Code Quality
No new issues found. Observations:
LoadMetaBookdropMergeOrdererror atshell_handler.go:337is consistent with the established pattern for optional/degraded-mode loads (LoadMetaLLMVision,LoadMetaLibWeightOverrides).LoadBookdropMergeChainalways returns the default chain on missing key, so the error path is only unexpected DB failure — silencing it matches the rest of the function.e2e/browser/journey_bookdrop_merge_order_test.go:176-177has twoExpectcalls in oneIt. This is correct under the documented e2e multi-Expect relaxation (CLAUDE.md: "journey It steps may have multiple Expect calls — a single action whose result spans…").e2e/browser/journey_bookdrop_merge_order_test.go:184-195—It("captures a screenshot…")includes a DOM assertion (hasDraggable) so it is not a screenshot-only prohibitedIt.e2e/browser/is 26 top-levelDescribeblocks. Policy budget is ~12 (CLAUDE.md). This is above the stated budget — but the new journey is one of 26, not newly violating; this is an existing fleet concern, not introduced by this PR.No blockers, no majors, no minors remaining.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
67443cebed8efff50e2f