Review follow-ups: PR #525 — feature: automatic sized binders, QR purchase requests and branded covers #771
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Non-blocking findings from the auto code review of PR #525 (feature: automatic sized binders, QR purchase requests and branded covers) — captured for batch cleanup rather than blocking the PR.
Commit
2253d45· https://github.com/Gambit-Inc/gambit/pull/525SHOULD-FIX:
frontend/app/binder/[token]/page.js:~603(<BinderCover binder={profile?.binder} …/>on the legacy path): the cover now renders on every existing shared binder page, not only sized ones. Legacy binders havesettings = null, soBinderCovershows a blue#1457e6banner. It repeats the binder name and adds "A collection worth sharing." and the "Powered by Gambit" badge. This is a visible production change to every existing public binder. It also contradictsdocs/binders-beta-ip.md, which says existing binders "keep legacy rendering until configured". Render it only whenbinder.settingsis set, or drop this line, since the sized path already returnsPublicSmartBinder.backend/app/services/binder_automation.pyassign_items(called attransactions.py:5054,intake.py:765,imports.py:1804): every finalize, intake commit and import chunk now runs an inventory/product join plus an automationSELECT … FOR UPDATEfor each touched item, even when the vendor has no automations. Two consequences:db.begin_nested()with logging, or accept the coupling knowingly.FOR UPDATEon all of a vendor's enabled automations, held until commit, serializes concurrent finalizes for that vendor.smart_binders.pycreate_automation: it callsdb.run_sync(assign_inventory)once per item, up to 5,000, in a single HTTP request. Each call does 4 to 6 queries and takes the automation row lock. That is potentially 20k+ round trips inside one request while the vendor's POS finalizations block on the lock. Batch it: load the items and products once, then compute the assignments in memory. Alternatively, capfill_existingfar lower or move it to a background job.binders.py:403delete_binder: the delete now cascades through the newON DELETE CASCADEFKs. It removesbinder_customer_requests, which are the vendor's pull-request records with customer contact details. It also removesbinder_cover_uploadsand, through them,binder_cover_reports. That contradictsdocs/binders-beta-ip.md, which says "audit records remain until workspace deletion or the adopted retention procedure". Decide deliberately:binder_idon requests and uploadsSET NULLorRESTRICT.Separately, deleting an automated binder is allowed while all item edits on it return 409. Its stock is not re-filed until a new scan, because the
fill_existingbackfill only runs at rule creation.core.py:1304_auto_publish_to_binder(existing code, but this PR makes it unsafe): a manual binder that has opted into sizedsettingsand hasauto_publish_new_inventory=Truestill getsmax_sort+10with no capacity check. Items can land pastpockets*sheets*2, andPublicSmartBinderonly renderssheets*2pages, so those cards silently never appear.add_binder_itemsgot the capacity guard and this path did not. Either skip or guard whenbinder.settingsis set, or reject enabling auto-publish on a sized binder.NITS:
backend/app/routes/auth.py:352: the local import re-importsBinderCoverUpload, which is already imported at module level (line 46). It is redundant and shadows the module-level name inside the function.frontend/app/binders/automatic/page.jsx, cover title/subtitle inputs:list={binder-filter-${key}}points at datalists that don't exist fortitleandsubtitle. It is harmless but dead.frontend/app/binders/automatic/page.jsx, preset<select defaultValue="custom">: choosing "Custom filters" resetsrulesto{}. That wipes any filters the user has already typed, and the control is uncontrolled.frontend/app/binders/BindersPageDropdown.jsx:~367:setItemView(...)runs on every binder load. A view the user picked is reset after each refresh of a sized binder.public_binder_itemsre-serializes the whole binder for every 300-item page. Sized binders can hold up to 7,200 slots, soPublicSmartBinder's paging loop does O(n²/300) work server-side.frontend/app/components/binders/PublicSmartBinder.jsx,post():res.json()runs before theres.okcheck. A non-JSON error such as a proxy 502 or 429 body surfaces as a raw JSON parse error to the customer.Filed from a Claude Code session