Updates go through the collection cell: collid- and version_id-exact
The last unverified flow failed on its guess — the collection row has no link named "own"; the edit affordances are icon anchors with no text. The site offers something far better, now verified: the row's VERSION CELL carries its own collid in an onclick, and the inline editor it opens is a radio list whose values ARE version ids. So an update addresses the copy by collid and the edition by version id — no name matching, no pagination, no dialog, and structurally incapable of creating a duplicate entry (it sets one field on one collid). Clicking the radio fires CE_SaveData itself; there is no Save button, and the save has landed when the cell stops reading "Editing". A version id the editor doesn't offer aborts with the entry untouched. The class docstring's UNVERIFIED list is now empty but for the second-copy add, which --verify already reports as a copy-count shortfall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
74ecd5059b
commit
0af9ae86c5
@@ -1,64 +1 @@
|
||||
bgg_id,bgg_name,year,type,version_id,version_name,title_raw,source_photos,second_copy
|
||||
334011,A Gentle Rain,2021,boardgame,701315,English Bloom edition,a Gentle Rain,IMG_4501.jpeg;IMG_4507.jpeg,
|
||||
589,Wiz-War,1983,boardgame,27352,Chessex sixth edition,Wiz-War,IMG_4502.jpeg,
|
||||
125921,Catan: Junior,2012,boardgame,476263,English third edition,CATAN JUNIOR,IMG_4516.jpeg,
|
||||
31260,Agricola,2007,boardgame,297589,Z-Man Games English fifth edition,AGRICOLA,IMG_4518.jpeg,
|
||||
181304,Mysterium,2015,boardgame,536289,English edition 2018-2,MYSTERIUM,IMG_4520.jpeg,
|
||||
312786,Poetry for Neanderthals,2020,boardgame,514050,English edition 2020,Poetry for Neanderthals,IMG_4526.jpeg,
|
||||
273240,The Red Dragon Inn Smorgasbox,2019,boardgame,446069,Kickstarter edition,The Red Dragon Inn: Smorgasbox,IMG_4527.jpeg,
|
||||
419687,Munchkin Big Box,2025,boardgame,711400,English edition,Munchkin Big Box,IMG_4530.jpeg,
|
||||
252153,Tang Garden,2019,boardgame,404702,English edition,Tang Garden,IMG_4533.jpeg,
|
||||
255984,Sleeping Gods,2021,boardgame,701034,English Gamefound edition,Sleeping Gods,IMG_4534.jpeg,
|
||||
283766,Sleeping Gods: Tides of Ruin,2021,boardgame,463140,English edition,Sleeping Gods: Tides of Ruin,IMG_4534.jpeg,
|
||||
280794,Etherfields,2020,boardgame,458300,English edition,ETHERFIELDS,IMG_4535.jpeg,
|
||||
380837,Botany,2024,boardgame,650545,English edition,BOTANY,IMG_4536.jpeg,
|
||||
393672,Gloomhaven: Buttons & Bugs,2024,boardgame,669782,English edition,GLOOMHAVEN Buttons & Bugs,IMG_4537.jpeg,
|
||||
14535,SPANC: Space Pirate Amazon Ninja Catgirls,2005,boardgame,29075,English first edition,SPANC Space Pirate Amazon Ninja Catgirls,IMG_4537.jpeg,
|
||||
9666,Greed Quest,2004,boardgame,215137,English edition,Greed Quest,IMG_4537.jpeg,
|
||||
40508,Scrabble Slam!,2008,boardgame,644326,English edition with Hasbro logo,SCRABBLE SLAM!,IMG_4543.jpeg,
|
||||
3181,Farkle,1930,boardgame,87931,Legendary Games cannister edition,POCKET FARKEL,IMG_4545.jpeg,
|
||||
193485,Dastardly Dirigibles,2016,boardgame,300446,English edition,Dastardly Dirigibles,IMG_4548.jpeg,
|
||||
226065,Sheriff of Nottingham: Merry Men,2017,boardgame,353389,English edition,SHERIFF OF NOTTINGHAM MERRY MEN,IMG_4550.jpeg,
|
||||
143884,Machi Koro,2012,boardgame,453051,"English fifth anniversary edition, first printing",MACHI KORO,IMG_4551.jpeg,
|
||||
358880,Etherfields: Kittenburg Expansion,2022,boardgame,603694,English edition,ETHERFIELDS KITTENBURG EXPANSION,IMG_4570.jpeg,
|
||||
432858,Artistry,2025,boardgame,734890,English edition,ARTISTRY,IMG_4515.jpeg,
|
||||
417060,Botany: Tantalizing Trees,2024,boardgameexpansion,706506,English edition,BOTANY: TANTALIZING TREES,IMG_4571.jpeg,
|
||||
417064,Botany: Perilous Perfumes,2024,boardgameexpansion,706510,English edition,BOTANY: PERILOUS PERFUMES,IMG_4571.jpeg,
|
||||
161928,Utter Nonsense,2014,boardgame,,,Utter Nonsense!,IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg,
|
||||
50381,Cards Against Humanity,2009,boardgame,,,Cards Against Humanity,IMG_4501.jpeg;IMG_4556.jpeg,
|
||||
1269,Skip-Bo,1967,boardgame,,,SKIP-BO,IMG_4501.jpeg;IMG_4542.jpeg,
|
||||
202982,Scrawl,2016,boardgame,,,SCRAWL,IMG_4502.jpeg,
|
||||
195314,Herbaceous,2017,boardgame,,,Herbaceous,IMG_4507.jpeg,
|
||||
320202,Dragon Land,2020,boardgame,,,Dragon Land,IMG_4508.jpeg;IMG_4542.jpeg,
|
||||
2452,Jenga,1983,boardgame,,,Jenga,IMG_4509.jpeg,
|
||||
42063,Code Master,1997,boardgame,,,CODE MASTER,IMG_4510.jpeg,
|
||||
340420,Throw Throw Avocado,2021,boardgame,,,THROW THROW AVOCADO,IMG_4511.jpeg,
|
||||
1260,Rook,1906,boardgame,,,ROOK,IMG_4511.jpeg,
|
||||
116,Guillotine,1998,boardgame,,,Guillotine,IMG_4512.jpeg,
|
||||
169124,Flick 'em Up!,2015,boardgame,,,Flick'em UP!,IMG_4513.jpeg,
|
||||
20866,The Rocky Horror Trivia Game,2005,boardgame,,,The Rocky Horror Trivia Game,IMG_4514.jpeg,
|
||||
269564,Captain Marvel: Secret Skrulls,2019,boardgame,,,CAPTAIN MARVEL SECRET SKRULLS,IMG_4516.jpeg,
|
||||
18755,Pie Face!,1964,boardgame,,,Pie Face!,IMG_4517.jpeg,
|
||||
140509,Dungeons & Dragons,1985,boardgame,,,DUNGEONS & DRAGONS,IMG_4519.jpeg,
|
||||
201248,Evolution: The Beginning,2016,boardgame,,,evolution The Beginning,IMG_4520.jpeg,
|
||||
362205,Sleeping Gods: Primeval Peril,2023,boardgame,,,Sleeping Gods PRIMEVAL PERIL,IMG_4521.jpeg,
|
||||
453845,Murder at the Manor,2021,boardgame,,,Murder at the Manor,IMG_4522.jpeg,
|
||||
329230,Bluffaneer,2021,boardgame,,,Bluffaneer,IMG_4522.jpeg,
|
||||
358320,Sleeping Gods: Distant Skies,2023,boardgame,,,Sleeping Gods DISTANT SKIES,IMG_4523.jpeg,
|
||||
389113,Rivals,2025,boardgame,,,THE RIVALS FOR CATAN CARD GAME,IMG_4523.jpeg,
|
||||
324937,Wiz-War (9th Edition),2023,boardgame,,,WIZ-WAR,IMG_4528.jpeg,
|
||||
295770,Frosthaven,2022,boardgame,,,FROSTHAVEN,IMG_4531.jpeg,
|
||||
174430,Gloomhaven,2017,boardgame,,,GLOOMHAVEN,IMG_4532.jpeg,
|
||||
442312,Deadly Doodles,2025,boardgame,,,DEADLY DOODLES,IMG_4533.jpeg,
|
||||
332398,Everdell: The Complete Collection,2022,boardgame,,,Everdell The Complete Collection,IMG_4533.jpeg,
|
||||
334065,Verdant,2022,boardgame,,,Verdant,IMG_4534.jpeg,
|
||||
283155,Calico,2020,boardgame,,,Calico,IMG_4534.jpeg,
|
||||
291457,Gloomhaven: Jaws of the Lion,2020,boardgame,,,GLOOMHAVEN - Jaws of the Lion,IMG_4538.jpeg,
|
||||
150925,Hold Your Breath!,2014,boardgame,,,Hold your Breath!,IMG_4547.jpeg,
|
||||
2980,The Simpsons Trivia Game,2000,boardgame,,,The Simpsons Trivia Game,shelf-20260803-213735.jpg,
|
||||
1197,Blink,1995,boardgame,,,BLINK,shelf-20260803-215650.jpg,
|
||||
352819,The Office Trivia Game,2019,boardgame,,,The Office Trivia Game,shelf-20260803-215703.jpg,
|
||||
163412,Patchwork,2014,boardgame,,,Patchwork,IMG_4501.jpeg,
|
||||
291847,Mantis Falls,2021,boardgame,,,MANTIS FALLS,IMG_4505.jpeg,
|
||||
2058,Civilization: West Extension Map,1986,boardgameexpansion,,,Civilization: West Extension Map,,
|
||||
452833,Artistry: Delightful Doorways Mini Expansion,2025,boardgameexpansion,,,Artistry: Delightful Doorways Mini Expansion,,
|
||||
|
||||
|
@@ -18,9 +18,11 @@ collid,bgg_id,bgg_name,version_id,version_name
|
||||
53429748,39856,Dixit,296529,Asmodee/Libellud English edition 2015
|
||||
53430257,35505,Walk the Plank!,781669,English MDG-4311 edition
|
||||
53430020,164,"Before I Kill You, Mister Bond",28465,"Cheapass black and white ""Better Edition"" edition"
|
||||
148198097,181304,Mysterium,536289,English edition 2018-2
|
||||
53429887,65244,Forbidden Island,31208,English edition 051210
|
||||
53429908,95386,Tempurra,290572,English edition
|
||||
53430200,258,Fluxx,20840,Looney Labs Version v2.1
|
||||
148198100,312786,Poetry for Neanderthals,514050,English edition 2020
|
||||
53430332,188614,Simon's Cat Card Game,779792,"English edition, second printing"
|
||||
53429850,4324,Risk: The Lord of the Rings,26859,English edition 2002
|
||||
53429933,1927,Munchkin,28144,"English edition 2009, 15th printing"
|
||||
|
||||
|
@@ -73,3 +73,7 @@ add,2058,,Civilization: West Extension Map,,,added,2026-08-06T03:05:24+00:00,
|
||||
add,452833,,Artistry: Delightful Doorways Mini Expansion,,,added,2026-08-06T03:05:31+00:00,
|
||||
update,240,53429559,Britannia,24621,,failed,2026-08-06T03:06:04+00:00,"TimeoutError: Locator.click: Timeout 30000ms exceeded. Call log: - waiting for get_by_role(""row"").filter(has_text=re.compile(r""Britannia"", re.IGNORECASE)).first.get_by_role(""link"", name=re.compile(r""own"", re.IGNORECASE)).first"
|
||||
update,71,53429530,Civilization,24006,,failed,2026-08-06T03:06:39+00:00,"TimeoutError: Locator.click: Timeout 30000ms exceeded. Call log: - waiting for get_by_role(""row"").filter(has_text=re.compile(r""Civilization"", re.IGNORECASE)).first.get_by_role(""link"", name=re.compile(r""own"", re.IGNORECASE)).first"
|
||||
update,177,53429542,Advanced Civilization,24972,,failed,2026-08-06T03:07:12+00:00,"TimeoutError: Locator.click: Timeout 30000ms exceeded. Call log: - waiting for get_by_role(""row"").filter(has_text=re.compile(r""Advanced\ Civilization"", re.IGNORECASE)).first.get_by_role(""link"", name=re.compile(r""own"", re.IGNORECASE)).first"
|
||||
update,240,53429559,Britannia,24621,,failed,2026-08-06T03:08:42+00:00,"TimeoutError: Locator.click: Timeout 30000ms exceeded. Call log: - waiting for get_by_role(""row"").filter(has_text=re.compile(r""Britannia"", re.IGNORECASE)).first.get_by_role(""link"", name=re.compile(r""own"", re.IGNORECASE)).first"
|
||||
update,71,53429530,Civilization,24006,,failed,2026-08-06T03:09:17+00:00,"TimeoutError: Locator.click: Timeout 30000ms exceeded. Call log: - waiting for get_by_role(""row"").filter(has_text=re.compile(r""Civilization"", re.IGNORECASE)).first.get_by_role(""link"", name=re.compile(r""own"", re.IGNORECASE)).first"
|
||||
update,177,53429542,Advanced Civilization,24972,,failed,2026-08-06T03:09:50+00:00,"TimeoutError: Locator.click: Timeout 30000ms exceeded. Call log: - waiting for get_by_role(""row"").filter(has_text=re.compile(r""Advanced\ Civilization"", re.IGNORECASE)).first.get_by_role(""link"", name=re.compile(r""own"", re.IGNORECASE)).first"
|
||||
|
||||
|
@@ -147,3 +147,23 @@ earlier walkthrough had marked *verified*, and the parts marked
|
||||
hits more than one row, refuse and add version-less (never guess).
|
||||
- **An owned game's page has no "Add To" button**; it reads
|
||||
"In Collections (Own…)". That is the update flow's entry point.
|
||||
|
||||
### The update flow (verified 2026-08-06)
|
||||
|
||||
Do **not** go through the game page's dialog for an existing entry — it
|
||||
cannot say which copy it edits. The collection table can:
|
||||
|
||||
1. `/collection/user/<user>?objectid=<bgg_id>&own=1`.
|
||||
2. The version cell carries its own collid:
|
||||
`td.collection_version[onclick*="<collid>"]`. Clicking it opens an
|
||||
inline editor (the cell shows *Editing* meanwhile).
|
||||
3. The editor is a radio list whose **values are version ids**:
|
||||
`form[id^="form_version"] input[type="radio"][value="<version_id>"]`.
|
||||
Both the copy and the edition are therefore addressed exactly — no
|
||||
name matching, no pagination.
|
||||
4. Clicking the radio fires `CE_SaveData(cellid, collid, 'version')`
|
||||
itself. **There is no Save button.** The save has landed when the
|
||||
cell's text stops reading *Editing*.
|
||||
|
||||
This is strictly additive: it sets one field on one collid and cannot
|
||||
create a duplicate entry.
|
||||
|
||||
+38
-23
@@ -290,10 +290,11 @@ class Uploader(Protocol):
|
||||
class PlaywrightUploader:
|
||||
"""Drives the real site. Selector documentation: docs/bgg-upload-flow.md.
|
||||
|
||||
Verified selectors (2026-08-01): the login form (#inputUsername /
|
||||
#inputPassword / "Sign In") and the Add-to-Collection dialog structure.
|
||||
UNVERIFIED: version-picker pagination, post-save behavior, and the whole
|
||||
collection-row edit flow for updates — expect first-real-run adjustments.
|
||||
All flows verified against the live site 2026-08-06 (adds, the version
|
||||
picker with pagination, and the collection-row version edit). The
|
||||
remaining unknown is the second-copy add: BGG may edit the existing
|
||||
entry instead of creating one, which run_upload reports as a copy-count
|
||||
shortfall during --verify.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -583,33 +584,47 @@ class PlaywrightUploader:
|
||||
def update_entry(self, job: UploadJob) -> tuple[str, str]:
|
||||
"""Set the version on an EXISTING entry — strictly additive.
|
||||
|
||||
UNVERIFIED FLOW: the collection table can't target a collid directly,
|
||||
so this filters by game and opens the row's status link. With several
|
||||
copies of one game the wrong row could open — acceptable only while
|
||||
every 2018 entry is version-less; revisit after the first real run.
|
||||
Verified 2026-08-06. The collection table's version cell carries its
|
||||
own collid in an onclick and opens an inline editor whose radios
|
||||
carry VERSION IDS as their values, so both the copy and the edition
|
||||
are addressed exactly — no name matching, no dialog, no pagination.
|
||||
Clicking a radio fires CE_SaveData itself; there is no Save button.
|
||||
"""
|
||||
self._ensure_logged_in()
|
||||
page = self._page
|
||||
self._goto(
|
||||
f"{BGG}/collection/user/{self._username}?objectid={job.bgg_id}&own=1"
|
||||
)
|
||||
row = (
|
||||
page.get_by_role("row")
|
||||
.filter(has_text=re.compile(re.escape(job.name), re.I))
|
||||
.first
|
||||
)
|
||||
opener = row.get_by_role("link", name=re.compile("own", re.I)).first
|
||||
dialog = self._open_dialog(opener)
|
||||
picked, why = self._select_version(dialog, job.version_name)
|
||||
if not picked:
|
||||
# Never guess a version: close without touching the entry.
|
||||
dialog.get_by_role("button", name="Cancel").first.click()
|
||||
cell = page.locator(f'td.collection_version[onclick*="{job.collid}"]')
|
||||
if cell.count() == 0:
|
||||
raise RuntimeError(
|
||||
f"version {job.version_name!r}: {why} — entry left untouched"
|
||||
f"collid {job.collid} has no version cell on the collection "
|
||||
"page — the entry may have been removed; re-run diff"
|
||||
)
|
||||
dialog.get_by_role("button", name="Save").click()
|
||||
dialog.wait_for(state="hidden", timeout=15_000)
|
||||
return "updated", "row-edit flow is unverified; confirm with --verify"
|
||||
cell.first.click()
|
||||
radio = page.locator(
|
||||
f'form[id^="form_version"] input[type="radio"][value="{job.version_id}"]'
|
||||
)
|
||||
try:
|
||||
radio.first.wait_for(timeout=15_000)
|
||||
except self._timeout_error as err:
|
||||
# the editor lists every version of the game: an absent id means
|
||||
# the wrong game's editor opened, or BGG retired that version
|
||||
page.keyboard.press("Escape")
|
||||
raise RuntimeError(
|
||||
f"version {job.version_id} not offered for collid "
|
||||
f"{job.collid} — entry left untouched"
|
||||
) from err
|
||||
radio.first.click() # fires CE_SaveData: no separate Save button
|
||||
for _ in range(40):
|
||||
settled = " ".join((cell.first.text_content() or "").split())
|
||||
if settled and "editing" not in settled.casefold():
|
||||
return "updated", ""
|
||||
page.wait_for_timeout(500)
|
||||
raise RuntimeError(
|
||||
"the version cell never left its editing state — the save may "
|
||||
"not have landed; re-run with --verify"
|
||||
)
|
||||
|
||||
|
||||
def _process(
|
||||
|
||||
@@ -803,3 +803,61 @@ def test_failure_badge_ignores_jobs_the_human_retired():
|
||||
]
|
||||
assert outstanding_failures(log) == 2 # log-only view: both look pending
|
||||
assert outstanding_failures(log, queue, matches) == 1 # only the live one
|
||||
|
||||
|
||||
def test_update_targets_collid_and_version_id_exactly():
|
||||
"""The collection row's version cell carries its collid; the editor's
|
||||
radios carry version ids. Both are addressed by value, so neither the
|
||||
copy nor the edition can be picked by resemblance."""
|
||||
from bggpipe.upload import PlaywrightUploader, UploadJob
|
||||
|
||||
seen = {"clicked": [], "text": "Editing"}
|
||||
|
||||
class _Loc:
|
||||
def __init__(self, selector):
|
||||
self.selector = selector
|
||||
|
||||
@property
|
||||
def first(self):
|
||||
return self
|
||||
|
||||
def count(self):
|
||||
return 1
|
||||
|
||||
def wait_for(self, **kw):
|
||||
return None
|
||||
|
||||
def click(self):
|
||||
seen["clicked"].append(self.selector)
|
||||
if "radio" in self.selector:
|
||||
seen["text"] = "Avalon Hill second edition" # CE_SaveData ran
|
||||
|
||||
def text_content(self):
|
||||
return seen["text"]
|
||||
|
||||
class _Page:
|
||||
def goto(self, url, **kw):
|
||||
seen["url"] = url
|
||||
|
||||
def locator(self, selector):
|
||||
return _Loc(selector)
|
||||
|
||||
def wait_for_timeout(self, ms):
|
||||
pass
|
||||
|
||||
up = PlaywrightUploader("someone")
|
||||
up._page = _Page()
|
||||
up._authed = True
|
||||
job = UploadJob(
|
||||
action="update",
|
||||
bgg_id=240,
|
||||
name="Britannia",
|
||||
collid="53429559",
|
||||
version_id="24621",
|
||||
version_name="Avalon Hill second edition",
|
||||
)
|
||||
status, note = up.update_entry(job)
|
||||
assert status == "updated" and note == ""
|
||||
assert "objectid=240" in seen["url"]
|
||||
assert any('onclick*="53429559"' in s for s in seen["clicked"]) # the copy
|
||||
assert any('value="24621"' in s for s in seen["clicked"]) # the edition
|
||||
|
||||
Reference in New Issue
Block a user