From 0af9ae86c58f91e62ebb2a8aeaeb5316d7171682 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Wed, 5 Aug 2026 23:15:39 -0400 Subject: [PATCH] Updates go through the collection cell: collid- and version_id-exact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g --- data/to_add.csv | 63 ----------------------------------------- data/to_update.csv | 2 ++ data/upload_log.csv | 4 +++ docs/bgg-upload-flow.md | 20 +++++++++++++ src/bggpipe/upload.py | 61 ++++++++++++++++++++++++--------------- tests/test_upload.py | 58 +++++++++++++++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 86 deletions(-) diff --git a/data/to_add.csv b/data/to_add.csv index 1104cc8..ee02d10 100644 --- a/data/to_add.csv +++ b/data/to_add.csv @@ -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,, diff --git a/data/to_update.csv b/data/to_update.csv index 1a2e7b7..c98b97a 100644 --- a/data/to_update.csv +++ b/data/to_update.csv @@ -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" diff --git a/data/upload_log.csv b/data/upload_log.csv index 9237f1b..811a678 100644 --- a/data/upload_log.csv +++ b/data/upload_log.csv @@ -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" diff --git a/docs/bgg-upload-flow.md b/docs/bgg-upload-flow.md index 096ef46..be866a5 100644 --- a/docs/bgg-upload-flow.md +++ b/docs/bgg-upload-flow.md @@ -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/?objectid=&own=1`. +2. The version cell carries its own collid: + `td.collection_version[onclick*=""]`. 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=""]`. + 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. diff --git a/src/bggpipe/upload.py b/src/bggpipe/upload.py index 62afc08..cbcdd08 100644 --- a/src/bggpipe/upload.py +++ b/src/bggpipe/upload.py @@ -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( diff --git a/tests/test_upload.py b/tests/test_upload.py index d278b74..0a6874d 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -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