Off-BGG games become local library citizens
An unmatched title that's a REAL game BGG doesn't have dead-ended: manual id or reject. The RPG local-citizen pattern generalizes to a human decision — review (web + TUI, key l) gains "not on BGG — keep locally": match_status "local" clears any BGG identity, diff routes it to local_only (never queued), and enrich synthesizes a library entry from the game's own photo reads (name, year, publisher cue — no API call, so even a blocked run lands them; pruning keeps local keys). Library and Titles show a "local — not on BGG" chip; Help's legend, review description, and shortcuts cover the new verb, distinguishing it from reject (bad read / not a game). 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
59f4b8c43c
commit
e187ed4f3f
+2
-5
@@ -29,7 +29,6 @@ TICKET TO RIDE,9209,Ticket to Ride,2004,boardgame,auto,,,version_ambiguous,"[{""
|
||||
CATAN,13,Catan,1995,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 13, ""name"": ""Catan"", ""year"": 1995, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 243104, ""rank"": 626}, {""bgg_id"": 66056, ""name"": ""Catan Двубоят"", ""year"": 2010, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 21829, ""rank"": 1187}]","[{""version_id"": 347121, ""name"": ""English edition 2016"", ""year"": 2016, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 495726, ""name"": ""English edition 2019"", ""year"": 2019, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 568647, ""name"": ""English edition 2020"", ""year"": 2020, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 733924, ""name"": ""English edition 2023"", ""year"": 2023, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 416798, ""name"": ""Afrikaans edition"", ""year"": 0, ""publishers"": [""Catan Studio""], ""languages"": [""Afrikaans""], ""score"": 2}]",IMG_4504.jpeg;IMG_4542.jpeg,,
|
||||
RAMPAGE,97903,Rampage,2013,boardgame,auto,,,version_unknown,"[{""bgg_id"": 97903, ""name"": ""Rampage"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4504.jpeg,,
|
||||
Onitama,160477,Onitama,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 160477, ""name"": ""Onitama"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4505.jpeg,,
|
||||
MANTIS FALLS (a game of trust),,,,,unmatched,,,,[],[],IMG_4505.jpeg,,
|
||||
Herbaceous,195314,Herbaceous,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 195314, ""name"": ""Herbaceous"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4507.jpeg,,
|
||||
LABYRINTH,1219,Labyrinth,1986,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 1219, ""name"": ""Labyrinth"", ""year"": 1986, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 35117, ""rank"": 2116}, {""bgg_id"": 6546, ""name"": ""Labyrinth"", ""year"": 1940, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1078, ""rank"": 31020}, {""bgg_id"": 23074, ""name"": ""Labyrinth"", ""year"": 2000, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 63, ""rank"": null}, {""bgg_id"": 24422, ""name"": ""Labyrinth"", ""year"": 2001, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 121, ""rank"": null}, {""bgg_id"": 65696, ""name"": ""Labyrinth"", ""year"": null, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 46, ""rank"": null}]","[{""version_id"": 488621, ""name"": ""English edition 2017"", ""year"": 2017, ""publishers"": [""Ravensburger AG""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 591493, ""name"": ""English/German edition"", ""year"": 2018, ""publishers"": [""Ravensburger AG""], ""languages"": [""English"", ""German""], ""score"": 3}, {""version_id"": 361899, ""name"": ""Multilingual 30th anniversary edition"", ""year"": 2016, ""publishers"": [""Ravensburger AG""], ""languages"": [""English"", ""French"", ""German"", ""Italian""], ""score"": 3}, {""version_id"": 338520, ""name"": ""Multilingual edition"", ""year"": 2017, ""publishers"": [""Ravensburger AG""], ""languages"": [""English"", ""French"", ""German"", ""Italian"", ""Portuguese"", ""Spanish""], ""score"": 3}, {""version_id"": 440013, ""name"": ""Multilingual edition"", ""year"": 2019, ""publishers"": [""Ravensburger AG""], ""languages"": [""Dutch"", ""English"", ""French"", ""German"", ""Italian""], ""score"": 3}, {""version_id"": 487001, ""name"": ""Multilingual edition"", ""year"": 2019, ""publishers"": [""Ravensburger AG""], ""languages"": [""Dutch"", ""English"", ""French"", ""German"", ""Italian"", ""Spanish""], ""score"": 3}, {""version_id"": 571233, ""name"": ""Multilingual edition"", ""year"": 2021, ""publishers"": [""Ravensburger AG""], ""languages"": [""Dutch"", ""English"", ""French"", ""German"", ""Italian"", ""Portuguese"", ""Spanish""], ""score"": 3}, {""version_id"": 630855, ""name"": ""Multilingual edition"", ""year"": 2022, ""publishers"": [""Ravensburger AG""], ""languages"": [""Dutch"", ""English"", ""French"", ""German"", ""Italian"", ""Portuguese"", ""Spanish""], ""score"": 3}]",IMG_4507.jpeg;IMG_4508.jpeg,,
|
||||
Super Mario Checkers Collector's Edition,,,,,unmatched,,,,[],[],IMG_4508.jpeg,,
|
||||
@@ -49,14 +48,12 @@ ROOK,1260,Rook,1906,boardgame,auto,,,version_unknown,"[{""bgg_id"": 1260, ""name
|
||||
Kensington,2197,Kensington,1979,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2197, ""name"": ""Kensington"", ""year"": 1979, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1207, ""rank"": 29937}, {""bgg_id"": 276459, ""name"": ""Kensington"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 99, ""rank"": 20091}]",[],IMG_4512.jpeg,,
|
||||
Guillotine,116,Guillotine,1998,boardgame,auto,,,version_unknown,"[{""bgg_id"": 116, ""name"": ""Guillotine"", ""year"": 1998, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 34783, ""rank"": 1630}, {""bgg_id"": 11629, ""name"": ""Guillotine"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 87, ""rank"": 18073}, {""bgg_id"": 27373, ""name"": ""Guillotine"", ""year"": 1987, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 163, ""rank"": 19576}]",[],IMG_4512.jpeg,,
|
||||
THE DOOM THAT CAME TO ATLANTIC CITY,124548,The Doom That Came to Atlantic City,2013,boardgame,auto,,,version_unknown,"[{""bgg_id"": 124548, ""name"": ""The Doom That Came to Atlantic City"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4513.jpeg,,
|
||||
SMALLWORLD,,,,,unmatched,,,,[],[],IMG_4513.jpeg,,
|
||||
Flick'em UP!,169124,Flick 'em Up!,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 169124, ""name"": ""Flick 'em Up!"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4513.jpeg,,
|
||||
Castle Panic,43443,Castle Panic,2009,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 43443, ""name"": ""Castle Panic"", ""year"": 2009, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 468123, ""name"": ""English edition, eighteenth printing"", ""year"": 2018, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 222131, ""name"": ""English edition, eighth printing"", ""year"": 2013, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 265959, ""name"": ""English edition, eleventh printing"", ""year"": 2014, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 343185, ""name"": ""English edition, fifteenth printing"", ""year"": 2016, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 169933, ""name"": ""English edition, fifth printing"", ""year"": 2012, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 24624, ""name"": ""English edition, first printing"", ""year"": 2009, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 337551, ""name"": ""English edition, fourteenth printing"", ""year"": 2016, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 103068, ""name"": ""English edition, fourth printing"", ""year"": 2010, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4513.jpeg,,
|
||||
The Red Dragon Inn,24310,The Red Dragon Inn,2007,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 24310, ""name"": ""The Red Dragon Inn"", ""year"": 2007, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 16870, ""rank"": 2164}, {""bgg_id"": 33451, ""name"": ""The Red Dragon Inn 2"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 93.3, ""owned"": 8355, ""rank"": 2056}, {""bgg_id"": 66510, ""name"": ""The Red Dragon Inn 3"", ""year"": 2011, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 93.3, ""owned"": 6246, ""rank"": 2363}, {""bgg_id"": 142402, ""name"": ""The Red Dragon Inn 4"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 93.3, ""owned"": 4107, ""rank"": 3367}, {""bgg_id"": 455834, ""name"": ""The Red Dragon Inn 10"", ""year"": 2026, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 90.3, ""owned"": 162, ""rank"": null}]","[{""version_id"": 400341, ""name"": ""English eighth edition"", ""year"": 2017, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 620786, ""name"": ""English eleventh edition"", ""year"": 2021, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 762888, ""name"": ""English fifteenth edition"", ""year"": 2024, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 374258, ""name"": ""English fifth edition"", ""year"": 2012, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 33932, ""name"": ""English first edition"", ""year"": 2007, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 728027, ""name"": ""English fourteenth edition"", ""year"": 2023, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 312374, ""name"": ""English seventh edition"", ""year"": 2015, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 389768, ""name"": ""English sixth edition"", ""year"": 2014, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4514.jpeg,,
|
||||
Dixit,39856,Dixit,2008,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 39856, ""name"": ""Dixit"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 482308, ""name"": ""Afrikaans/English edition"", ""year"": 2019, ""publishers"": [""Libellud""], ""languages"": [""Afrikaans"", ""English""], ""score"": 3}, {""version_id"": 488774, ""name"": ""Arabic/English edition"", ""year"": 2019, ""publishers"": [""Boardgame Space"", ""Libellud""], ""languages"": [""Arabic"", ""English""], ""score"": 3}, {""version_id"": 296529, ""name"": ""Asmodee/Libellud English edition 2015"", ""year"": 2015, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 160064, ""name"": ""English edition 2011"", ""year"": 2011, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 193872, ""name"": ""English edition 2012"", ""year"": 2012, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 241272, ""name"": ""English edition 2013"", ""year"": 2013, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 256721, ""name"": ""English edition 2014"", ""year"": 2014, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 273210, ""name"": ""English edition 2014"", ""year"": 2014, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}]",IMG_4514.jpeg,,
|
||||
Huggermugger,2387,Huggermugger,1989,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2387, ""name"": ""Huggermugger"", ""year"": 1989, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4514.jpeg,,
|
||||
The Rocky Horror Trivia Game,20866,The Rocky Horror Trivia Game,2005,boardgame,auto,,,version_unknown,"[{""bgg_id"": 20866, ""name"": ""The Rocky Horror Trivia Game"", ""year"": 2005, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4514.jpeg,,
|
||||
ART NOUVEAU ARTISTRY,,,,,unmatched,,,,[],[],IMG_4515.jpeg,,
|
||||
SPEECHLESS,196105,Speechless,2016,boardgame,approved,,,version_unknown,"[{""bgg_id"": 14370, ""name"": ""Speechless"", ""year"": 1985, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 94, ""rank"": null}, {""bgg_id"": 196105, ""name"": ""Speechless"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 493, ""rank"": 10370}]",[],IMG_4516.jpeg,,
|
||||
CATAN JUNIOR,125921,Catan: Junior,2012,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 184842, ""name"": ""Catan Junior"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 3961, ""rank"": 4440}, {""bgg_id"": 5824, ""name"": ""Catan Junior"", ""year"": 2003, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1225, ""rank"": 30649}, {""bgg_id"": 125921, ""name"": ""Catan: Junior"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 9917, ""rank"": 2616}]","[{""version_id"": 791740, ""name"": ""English edition 2025"", ""year"": 2025, ""publishers"": [""Catan GmbH"", ""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 558911, ""name"": ""English fourth edition"", ""year"": 2018, ""publishers"": [""Catan GmbH"", ""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 414618, ""name"": ""English second edition"", ""year"": 2016, ""publishers"": [""Catan GmbH"", ""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 476263, ""name"": ""English third edition"", ""year"": 2016, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}]",IMG_4516.jpeg,,
|
||||
SKYLANDERS PORTAL MASTER,156994,Skylanders Portal Master,2013,boardgame,auto,,,version_unknown,"[{""bgg_id"": 156994, ""name"": ""Skylanders Portal Master"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4516.jpeg,,
|
||||
@@ -66,7 +63,6 @@ Pie Face!,18755,Pie Face!,1964,boardgame,approved,,,version_unknown,"[{""bgg_id"
|
||||
Walk the Plank!,35505,Walk the Plank!,2013,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 21834, ""name"": ""Walk the Plank"", ""year"": 2002, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 18, ""rank"": null}, {""bgg_id"": 29010, ""name"": ""Walk the Plank"", ""year"": 2007, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 179, ""rank"": 28080}, {""bgg_id"": 152950, ""name"": ""Walk the Plank"", ""year"": null, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 5, ""rank"": null}, {""bgg_id"": 35505, ""name"": ""Walk the Plank!"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 3127, ""rank"": 3700}]","[{""version_id"": 781669, ""name"": ""English MDG-4311 edition"", ""year"": 2013, ""publishers"": [""Mayday Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 189968, ""name"": ""English MYY4311 edition"", ""year"": 2013, ""publishers"": [""Mayday Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 483159, ""name"": ""Russian edition"", ""year"": 2013, ""publishers"": [""Cosmodrome Games"", ""Mayday Games""], ""languages"": [""Russian""], ""score"": 2}]",IMG_4517.jpeg;IMG_4549.jpeg,,
|
||||
AGRICOLA,31260,Agricola,2007,boardgame,auto,297589,Z-Man Games English fifth edition,version_auto,"[{""bgg_id"": 31260, ""name"": ""Agricola"", ""year"": 2007, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 297589, ""name"": ""Z-Man Games English fifth edition"", ""year"": 2013, ""publishers"": [""Filosofia Éditions"", ""Lookout Games"", ""Z-Man Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 165308, ""name"": ""999 Games Dutch second edition 2011"", ""year"": 2011, ""publishers"": [""999 Games"", ""Lookout Games""], ""languages"": [""Dutch""], ""score"": 2}, {""version_id"": 266028, ""name"": ""Hobby World Russian first edition"", ""year"": 2013, ""publishers"": [""Hobby World"", ""Lookout Games""], ""languages"": [""Russian""], ""score"": 2}, {""version_id"": 22673, ""name"": ""Lookout Games German first edition"", ""year"": 2007, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 64715, ""name"": ""Lookout Games German second edition 2008 with animeeples"", ""year"": 2008, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 219203, ""name"": ""Lookout Games German seventh edition 2012 with DSP and SdJ"", ""year"": 2012, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 64718, ""name"": ""Lookout Games German third edition 2009 with DSP and SdJ"", ""year"": 2009, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 113858, ""name"": ""Lookout Games Russian first edition 2011"", ""year"": 2011, ""publishers"": [""Lookout Games"", ""Smart Ltd""], ""languages"": [""Russian""], ""score"": 2}]",IMG_4518.jpeg,,
|
||||
METEOR,166109,Meteor,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 25172, ""name"": ""Meteor"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 56, ""rank"": null}, {""bgg_id"": 166109, ""name"": ""Meteor"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1396, ""rank"": 10371}, {""bgg_id"": 246740, ""name"": ""Meteors"", ""year"": 2018, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 92.3, ""owned"": 71, ""rank"": 25541}]",[],IMG_4518.jpeg,,
|
||||
MAGE WARS ACADEMY CORE SET,,,,,unmatched,,,,[],[],IMG_4518.jpeg,,
|
||||
DOCTOR WHO THE card GAME,125675,Doctor Who: The Card Game,2012,boardgame,auto,,,version_unknown,"[{""bgg_id"": 125675, ""name"": ""Doctor Who: The Card Game"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4518.jpeg,,
|
||||
DUNGEONS & DRAGONS,140509,Dungeons & Dragons,1985,boardgame,auto,,,version_unknown,"[{""bgg_id"": 140509, ""name"": ""Dungeons & Dragons"", ""year"": 1985, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4519.jpeg,,
|
||||
DUNGEONS & DRAGONS FANTASY GAME RULES,,,,,unmatched,,,,[],[],IMG_4519.jpeg,,
|
||||
@@ -110,7 +106,7 @@ Greed Quest,9666,Greed Quest,2004,boardgame,auto,215137,English edition,version_
|
||||
GLOOMHAVEN - Jaws of the Lion,291457,Gloomhaven: Jaws of the Lion,2020,boardgame,auto,,,version_unknown,"[{""bgg_id"": 291457, ""name"": ""Gloomhaven: Jaws of the Lion"", ""year"": 2020, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4538.jpeg,,
|
||||
SCRABBLE SLAM!,40508,Scrabble Slam!,2008,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 40508, ""name"": ""Scrabble Slam!"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 39335, ""name"": ""English edition"", ""year"": 2009, ""publishers"": [""Hasbro""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 644326, ""name"": ""English edition with Hasbro logo"", ""year"": 2008, ""publishers"": [""Hasbro"", ""Parker Brothers""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 25061, ""name"": ""English edition with Parker Brothers logo"", ""year"": 2008, ""publishers"": [""Hasbro"", ""Parker Brothers""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 294332, ""name"": ""English/French edition"", ""year"": 2008, ""publishers"": [""Hasbro"", ""Parker Brothers""], ""languages"": [""English"", ""French""], ""score"": 3}, {""version_id"": 480950, ""name"": ""English/French edition"", ""year"": 2014, ""publishers"": [""Hasbro""], ""languages"": [""English"", ""French""], ""score"": 3}, {""version_id"": 116530, ""name"": ""Dutch edition"", ""year"": 2009, ""publishers"": [""Hasbro""], ""languages"": [""Dutch""], ""score"": 2}, {""version_id"": 345823, ""name"": ""French edition"", ""year"": 2009, ""publishers"": [""Hasbro""], ""languages"": [""French""], ""score"": 2}, {""version_id"": 111167, ""name"": ""German edition"", ""year"": 2010, ""publishers"": [""Hasbro""], ""languages"": [""German""], ""score"": 2}]",IMG_4543.jpeg,,
|
||||
CAT STAX,,,,,unmatched,,,,[],[],IMG_4544.jpeg,,
|
||||
POCKET FARKEL,,,,,unmatched,,,,[],[],IMG_4545.jpeg,,
|
||||
POCKET FARKEL,3181,Farkle,1930,boardgame,approved,,,version_ambiguous,[],"[{""version_id"": 374844, ""name"": ""Legendary Games black flat pack edition"", ""year"": 1996, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 390425, ""name"": ""Legendary Games blister pack editon"", ""year"": 0, ""publishers"": [""Cardinal"", ""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 87931, ""name"": ""Legendary Games cannister edition"", ""year"": 0, ""publishers"": [""Legendary Games, Inc."", ""Reveal Entertainment, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 295385, ""name"": ""Legendary Games dice rolling tray edition"", ""year"": 2006, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 658688, ""name"": ""Legendary Games English/Spanish \""Alaska: The Last Frontier\"" edition"", ""year"": 1999, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English"", ""Spanish""], ""score"": 3}, {""version_id"": 374847, ""name"": ""Legendary Games flat pack edition"", ""year"": 0, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 247184, ""name"": ""Legendary Games flat pack edition 1996"", ""year"": 1996, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 374842, ""name"": ""Legendary Games flat pack edition 1996"", ""year"": 1996, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}]",IMG_4545.jpeg,,
|
||||
Go Fish Yourself,173057,Go Fish Yourself,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 173057, ""name"": ""Go Fish Yourself"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4546.jpeg,,
|
||||
Hold your Breath!,150925,Hold Your Breath!,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 150925, ""name"": ""Hold Your Breath!"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4547.jpeg,,
|
||||
Dastardly Dirigibles,193485,Dastardly Dirigibles,2016,boardgame,auto,300446,English edition,version_auto,"[{""bgg_id"": 193485, ""name"": ""Dastardly Dirigibles"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 300446, ""name"": ""English edition"", ""year"": 2016, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4548.jpeg,,
|
||||
@@ -131,3 +127,4 @@ The Simpsons Trivia Game,2980,The Simpsons Trivia Game,2000,boardgame,auto,,,ver
|
||||
THE CAT GAME,231302,The Cat Game,2017,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 231302, ""name"": ""The Cat Game"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 361880, ""name"": ""English/French edition"", ""year"": 2017, ""publishers"": [""Spin Master Ltd.""], ""languages"": [""English"", ""French""], ""score"": 2}, {""version_id"": 433263, ""name"": ""German edition"", ""year"": 0, ""publishers"": [""Spin Master Ltd.""], ""languages"": [""German""], ""score"": 2}]",shelf-20260803-213756.jpg,,
|
||||
BLINK,1197,Blink,1995,boardgame,auto,,,version_unknown,"[{""bgg_id"": 1197, ""name"": ""Blink"", ""year"": 1995, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 6143, ""rank"": 6149}, {""bgg_id"": 185341, ""name"": ""Blink!"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 111, ""rank"": null}, {""bgg_id"": 15852, ""name"": ""Blinki"", ""year"": 1910, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 90.9, ""owned"": 106, ""rank"": 30046}]",[],shelf-20260803-215650.jpg,,
|
||||
The Office Trivia Game,352819,The Office Trivia Game,2019,boardgame,approved,,,version_error,"[{""bgg_id"": 352819, ""name"": ""The Office Trivia Game"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 159, ""rank"": null}, {""bgg_id"": 36895, ""name"": ""The Office Trivia Game"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 432, ""rank"": 30024}]",[],shelf-20260803-215703.jpg,,
|
||||
Patchwork,163412,Patchwork,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 163412, ""name"": ""Patchwork"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 124114, ""rank"": 148}, {""bgg_id"": 41585, ""name"": ""Patchwork"", ""year"": 2009, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 473, ""rank"": 14984}]",[],IMG_4501.jpeg,,
|
||||
|
||||
|
@@ -57,5 +57,25 @@
|
||||
"match": "Patch Work",
|
||||
"title_raw": "Patchwork",
|
||||
"confidence": "high"
|
||||
},
|
||||
{
|
||||
"match": "SMALLWORLD",
|
||||
"title_raw": "SMALL WORLD",
|
||||
"confidence": "high"
|
||||
},
|
||||
{
|
||||
"match": "ART NOUVEAU ARTISTRY",
|
||||
"title_raw": "ARTISTRY",
|
||||
"confidence": "high"
|
||||
},
|
||||
{
|
||||
"match": "MAGE WARS ACADEMY CORE SET",
|
||||
"title_raw": "MAGE WARS ACADEMY",
|
||||
"confidence": "high"
|
||||
},
|
||||
{
|
||||
"match": "MANTIS FALLS (a game of trust)",
|
||||
"title_raw": "MANTIS FALLS",
|
||||
"confidence": "high"
|
||||
}
|
||||
]
|
||||
|
||||
+8
-8
@@ -417,7 +417,7 @@
|
||||
"title_normalized": "onitama"
|
||||
},
|
||||
{
|
||||
"title_raw": "MANTIS FALLS (a game of trust)",
|
||||
"title_raw": "MANTIS FALLS",
|
||||
"confidence": "high",
|
||||
"publisher_hint": "Diligent Games",
|
||||
"edition_hint": "",
|
||||
@@ -427,7 +427,7 @@
|
||||
"source_photos": [
|
||||
"IMG_4505.jpeg"
|
||||
],
|
||||
"title_normalized": "mantis falls game of trust"
|
||||
"title_normalized": "mantis falls"
|
||||
},
|
||||
{
|
||||
"title_raw": "Herbaceous",
|
||||
@@ -680,7 +680,7 @@
|
||||
"title_normalized": "doom that came to atlantic city"
|
||||
},
|
||||
{
|
||||
"title_raw": "SMALLWORLD",
|
||||
"title_raw": "SMALL WORLD",
|
||||
"confidence": "high",
|
||||
"publisher_hint": "Days of Wonder",
|
||||
"edition_hint": "",
|
||||
@@ -690,7 +690,7 @@
|
||||
"source_photos": [
|
||||
"IMG_4513.jpeg"
|
||||
],
|
||||
"title_normalized": "smallworld"
|
||||
"title_normalized": "small world"
|
||||
},
|
||||
{
|
||||
"title_raw": "Flick'em UP!",
|
||||
@@ -771,7 +771,7 @@
|
||||
"title_normalized": "rocky horror trivia game"
|
||||
},
|
||||
{
|
||||
"title_raw": "ART NOUVEAU ARTISTRY",
|
||||
"title_raw": "ARTISTRY",
|
||||
"confidence": "high",
|
||||
"publisher_hint": "Dux Somnium",
|
||||
"edition_hint": "",
|
||||
@@ -781,7 +781,7 @@
|
||||
"source_photos": [
|
||||
"IMG_4515.jpeg"
|
||||
],
|
||||
"title_normalized": "art nouveau artistry"
|
||||
"title_normalized": "artistry"
|
||||
},
|
||||
{
|
||||
"title_raw": "SPEECHLESS",
|
||||
@@ -902,7 +902,7 @@
|
||||
"title_normalized": "meteor"
|
||||
},
|
||||
{
|
||||
"title_raw": "MAGE WARS ACADEMY CORE SET",
|
||||
"title_raw": "MAGE WARS ACADEMY",
|
||||
"confidence": "high",
|
||||
"publisher_hint": "Arcane Wonders",
|
||||
"edition_hint": "Core Set",
|
||||
@@ -912,7 +912,7 @@
|
||||
"source_photos": [
|
||||
"IMG_4518.jpeg"
|
||||
],
|
||||
"title_normalized": "mage wars academy core set"
|
||||
"title_normalized": "mage wars academy"
|
||||
},
|
||||
{
|
||||
"title_raw": "DOCTOR WHO THE card GAME",
|
||||
|
||||
+8
-1
@@ -135,6 +135,11 @@ def compute_diff(rows: list[dict], collection: list[CollectionItem]) -> DiffResu
|
||||
if status == "merged":
|
||||
result.merged += 1 # represented by its survivor row
|
||||
continue
|
||||
if status == "local":
|
||||
# the human's call: a real game BGG doesn't have — a library
|
||||
# citizen only, never queued
|
||||
result.local_only.append(row["title_raw"])
|
||||
continue
|
||||
if status not in RECOGNIZED_MATCH_STATUSES or not row["bgg_id"]:
|
||||
result.pending.append(row["title_raw"])
|
||||
continue
|
||||
@@ -316,7 +321,9 @@ def run_diff(cfg: Config, *, client: BGGClient | None = None) -> DiffResult:
|
||||
|
||||
merged_note = f" · {result.merged} merged duplicate(s)" if result.merged else ""
|
||||
local_note = (
|
||||
f" · {len(result.local_only)} local-only (RPGs)" if result.local_only else ""
|
||||
f" · {len(result.local_only)} local-only (RPGs, off-BGG games)"
|
||||
if result.local_only
|
||||
else ""
|
||||
)
|
||||
typer.echo(
|
||||
f"\n{result.recognized} recognized · {len(result.already_owned)} already "
|
||||
|
||||
+31
-2
@@ -28,7 +28,8 @@ from bggpipe.bgg_client import (
|
||||
from bggpipe.config import Config
|
||||
from bggpipe.fsio import atomic_write_text
|
||||
from bggpipe.models import is_confident_version, is_recognized
|
||||
from bggpipe.resolve import read_matches
|
||||
from bggpipe.normalize import normalize_title
|
||||
from bggpipe.resolve import load_titles, read_matches
|
||||
|
||||
BATCH_SIZE = 20
|
||||
|
||||
@@ -95,12 +96,40 @@ def run_enrich(
|
||||
games[key] = {**fetched[bgg_id], "version": version}
|
||||
updated += 1
|
||||
|
||||
# games the human ruled off-BGG: library entries built from their own
|
||||
# photo reads — no API involved, so a blocked run still lands them
|
||||
local_keys: set[str] = set()
|
||||
local_rows = [r for r in rows if r["match_status"] == "local"]
|
||||
if local_rows:
|
||||
try:
|
||||
cues = {
|
||||
(e.title_raw, ";".join(e.source_photos)): e
|
||||
for e in load_titles(cfg.titles_path)
|
||||
}
|
||||
except FileNotFoundError:
|
||||
cues = {}
|
||||
for row in local_rows:
|
||||
key = f"local:{normalize_title(row['title_raw'])}:{row['source_photos']}"
|
||||
local_keys.add(key)
|
||||
entry = cues.get((row["title_raw"], row["source_photos"]))
|
||||
games[key] = {
|
||||
"bgg_id": None,
|
||||
"name": row["title_raw"],
|
||||
"year": entry.year_hint if entry else None,
|
||||
"type": "localgame",
|
||||
"publishers": [entry.publisher_hint]
|
||||
if entry and entry.publisher_hint
|
||||
else [],
|
||||
"source_photos": [p for p in row["source_photos"].split(";") if p],
|
||||
}
|
||||
updated += 1
|
||||
|
||||
# prune keys no current target claims: a row whose version was approved
|
||||
# after a bare-key run (or was later rejected) must not leave an orphan
|
||||
# entry in the frontend seed data. Only safe when nothing was blocked —
|
||||
# a token-less run knows too little to declare anything stale.
|
||||
if not blocked:
|
||||
current = {key for key, _, _ in targets}
|
||||
current = {key for key, _, _ in targets} | local_keys
|
||||
stale = [k for k in games if k not in current]
|
||||
for k in stale:
|
||||
del games[k]
|
||||
|
||||
+17
-3
@@ -357,6 +357,17 @@ class ReviewSession:
|
||||
row["match_status"] = "rejected"
|
||||
self._save(row)
|
||||
|
||||
def decide_local(self, row: dict) -> None:
|
||||
"""A real game BGG simply doesn't have: a local library citizen —
|
||||
listed and enriched from its own photo reads, never uploaded."""
|
||||
row["match_status"] = "local"
|
||||
row["bgg_id"] = ""
|
||||
row["bgg_name"] = ""
|
||||
row["version_status"] = ""
|
||||
row["version_id"] = ""
|
||||
row["version_name"] = ""
|
||||
self._save(row)
|
||||
|
||||
def open_version_ballot(self, row: dict) -> int:
|
||||
"""The human knows which printing a box is even when the photo
|
||||
showed no cues: put EVERY published version on the row's ballot
|
||||
@@ -443,10 +454,10 @@ class ReviewSession:
|
||||
while True:
|
||||
self._show_item(row, candidates)
|
||||
prompt = (
|
||||
"[1-N] pick (s)kip (r)eject (m <id>) manual BGG id "
|
||||
"(f <text>) re-search (q)uit > "
|
||||
"[1-N] pick (s)kip (r)eject (l)ocal — not on BGG "
|
||||
"(m <id>) manual BGG id (f <text>) re-search (q)uit > "
|
||||
if row["match_status"] == "unmatched"
|
||||
else "[1-N] pick (s)kip (r)eject (q)uit > "
|
||||
else "[1-N] pick (s)kip (r)eject (l)ocal — not on BGG (q)uit > "
|
||||
)
|
||||
answer = self._ask(prompt)
|
||||
lowered = answer.lower()
|
||||
@@ -455,6 +466,9 @@ class ReviewSession:
|
||||
if lowered == "r":
|
||||
self.decide_reject(row)
|
||||
return
|
||||
if lowered == "l":
|
||||
self.decide_local(row)
|
||||
return
|
||||
if answer.isdigit() and 1 <= int(answer) <= len(candidates):
|
||||
self.decide_pick(row, candidates[int(answer) - 1])
|
||||
return
|
||||
|
||||
@@ -155,6 +155,7 @@ function statusChip(c) {
|
||||
if (c.status === "awaiting_resolve") return `<span class="chip wait">awaiting BGG</span>`;
|
||||
if (c.status === "auto" || c.status === "approved") return `<span class="chip ok">${esc(c.status)}</span>`;
|
||||
if (c.status === "rejected") return `<span class="chip no">rejected</span>`;
|
||||
if (c.status === "local") return `<span class="chip open">local — not on BGG</span>`;
|
||||
if (c.status === "merged") return `<span class="chip merged" title="merged into ${esc(c.merged_into)}">merged → ${esc(c.merged_into)}</span>`;
|
||||
return `<span class="chip open">${esc(c.status)}</span>`;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<p><b><a href="/">Pipeline</a></b> — run stages one at a time and watch their live output. Shows what's blocking (missing keys, stub data) and the counts at every step.</p>
|
||||
<p><b><a href="/photos">Photos</a></b> — drag photos in, drop them in the <code>photos/</code> folder, or (on a paired phone) tap the drop zone and shoot straight from the camera; camera captures get unique <code>shelf-…</code> names so they can never overwrite each other. Each photo has its own page listing every title read from it and any reshoot tickets — boxes seen but not identified. Photograph those up close, drop the new shot in, and extract again. Re-uploading a photo under the same <i>file name</i> deliberately replaces it, and the next extract run re-reads it.</p>
|
||||
<p><b><a href="/titles">Titles</a></b> — every read off your shelves, alphabetized, with its status and photos. This is the proofread checkpoint: <a href="#curation">edit, split, remove</a>. Its badge counts <span class="chip shaky">shaky read</span> lines — the model wasn't sure and nothing has verified them; filter to them, then press <b>✓ looks right</b> or edit each one.</p>
|
||||
<p><b><a href="/review">Review</a></b> — the decisions only you can make: which game a title is, which edition a copy is, and whether two same-game reads are really one box (merges show a veto). Keyboard-first; see <a href="#keys">shortcuts</a>.</p>
|
||||
<p><b><a href="/review">Review</a></b> — the decisions only you can make: which game a title is, which edition a copy is, whether two same-game reads are really one box (merges show a veto), and whether an unmatched title is a real game BGG simply doesn't have (<b>keep locally</b>: it joins the Library, never uploads). Keyboard-first; see <a href="#keys">shortcuts</a>.</p>
|
||||
<p><b><a href="/queue">Queue</a></b> — exactly what upload will do (new entries and version upgrades) and the log of everything it has done. Nothing reaches BGG that isn't visible here first.</p>
|
||||
<p><b><a href="/library">Library</a></b> — your enriched collection: filter by board games or RPGs. RPG matches are identified and enriched but never uploaded — BGG collections can't hold them, so they stay local citizens.</p>
|
||||
</div>
|
||||
@@ -49,13 +49,14 @@
|
||||
<p><span class="chip ok">auto</span> matched confidently, no review needed. <span class="chip ok">approved</span> you picked the match yourself.</p>
|
||||
<p><span class="chip open">ambiguous</span> several plausible games — needs your pick on Review. <span class="chip open">unmatched</span> nothing plausible found — enter a BGG id or re-search on Review.</p>
|
||||
<p><span class="chip merged">merged</span> two reads judged to be the same physical box; the merge is veto-able on Review. <span class="chip merged">copy</span> one copy of a title you split.</p>
|
||||
<p><span class="chip no">rejected</span> you ruled it's not on BGG (or not a game worth matching); it stays listed but goes no further.</p>
|
||||
<p><span class="chip open">local — not on BGG</span> you ruled it's a real game BGG doesn't have: it joins the Library from its own photo reads, and never uploads.</p>
|
||||
<p><span class="chip no">rejected</span> you ruled it's a bad read or not worth matching; it stays listed but goes no further.</p>
|
||||
<p><span class="chip shaky">shaky read</span> the vision model wasn't sure of this transcription and nothing has verified it yet — these are what the Titles badge counts. Clear one by pressing its <b>✓ looks right</b> (the read is fine as-is) or by editing it (you fixed it). A BGG match also clears it: a wrong read wouldn't have matched.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="keys">Keyboard shortcuts</h2>
|
||||
<div class="card prose">
|
||||
<p><b>Review:</b> <kbd>j</kbd>/<kbd>k</kbd> move between cards · <kbd>1</kbd>–<kbd>9</kbd> pick a candidate · <kbd>r</kbd> reject · <kbd>m</kbd> manual BGG id · <kbd>u</kbd> edition unknown · <kbd>v</kbd> veto a merge.</p>
|
||||
<p><b>Review:</b> <kbd>j</kbd>/<kbd>k</kbd> move between cards · <kbd>1</kbd>–<kbd>9</kbd> pick a candidate · <kbd>r</kbd> reject · <kbd>l</kbd> keep local (not on BGG) · <kbd>m</kbd> manual BGG id · <kbd>u</kbd> edition unknown · <kbd>v</kbd> veto a merge.</p>
|
||||
<p><b>Photo pages:</b> <kbd>←</kbd>/<kbd>→</kbd> move between photos.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ function gameCard(g) {
|
||||
: `<div class="noart" aria-hidden="true">${esc((g.name || "?").charAt(0).toUpperCase())}</div>`}
|
||||
<div class="info">
|
||||
<div class="gname">${esc(g.name)}${g.year ? ` <span class="meta">(${esc(g.year)})</span>` : ""}
|
||||
${g.type === "rpgitem" ? `<span class="chip open">RPG · local only</span>` : ""}</div>
|
||||
${g.type === "rpgitem" ? `<span class="chip open">RPG · local only</span>` : ""}
|
||||
${g.type === "localgame" ? `<span class="chip open">not on BGG · local</span>` : ""}</div>
|
||||
<div class="gmeta">${[players, time, weight, rank].filter(Boolean).map(esc).join(" · ")}</div>
|
||||
${g.version ? `<div class="gmeta">${esc(g.version.name || "")}</div>` : ""}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<span id="sectionlinks"></span>
|
||||
<span class="keyhelp">
|
||||
<kbd>j</kbd>/<kbd>k</kbd> move · <kbd>1</kbd>–<kbd>9</kbd> pick ·
|
||||
<kbd>r</kbd> reject · <kbd>m</kbd> manual id · <kbd>u</kbd> unknown ·
|
||||
<kbd>r</kbd> reject · <kbd>l</kbd> keep local · <kbd>m</kbd> manual id · <kbd>u</kbd> unknown ·
|
||||
<kbd>v</kbd> veto merge
|
||||
</span>
|
||||
</div>
|
||||
@@ -76,6 +76,7 @@ function matchCard(row, idx) {
|
||||
<span><kbd>m</kbd> <input type="text" inputmode="numeric" placeholder="BGG id, then ⏎"
|
||||
aria-label="manual BGG id"></span>
|
||||
<button class="reject" title="press r"><kbd>r</kbd> reject — not a game / bad read</button>
|
||||
<button class="golocal" title="press l"><kbd>l</kbd> not on BGG — keep locally</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>`;
|
||||
@@ -179,6 +180,8 @@ function render() {
|
||||
});
|
||||
m.querySelectorAll(".reject").forEach(b => b.onclick = () =>
|
||||
decide(b.closest(".card"), "reject"));
|
||||
m.querySelectorAll(".golocal").forEach(b => b.onclick = () =>
|
||||
decide(b.closest(".card"), "local"));
|
||||
m.querySelectorAll(".unknown").forEach(b => b.onclick = () =>
|
||||
version(b.closest(".card"), "unknown"));
|
||||
m.querySelectorAll(".veto").forEach(b => b.onclick = () =>
|
||||
@@ -238,6 +241,7 @@ document.addEventListener("keydown", e => {
|
||||
if (li) version(card, "pick", Number(li.dataset.pickver));
|
||||
}
|
||||
else if (e.key === "r" && kind === "match") decide(card, "reject");
|
||||
else if (e.key === "l" && kind === "match") decide(card, "local");
|
||||
else if (e.key === "u" && kind === "version") version(card, "unknown");
|
||||
else if (e.key === "v" && kind === "merge") vetoMerge(card);
|
||||
else if (e.key === "m" && kind === "match") { card.querySelector("input")?.focus(); e.preventDefault(); }
|
||||
|
||||
@@ -618,6 +618,7 @@ def create_app(
|
||||
"ambiguous": counts.get("ambiguous", 0),
|
||||
"unmatched": counts.get("unmatched", 0),
|
||||
"rejected": counts.get("rejected", 0),
|
||||
"local": counts.get("local", 0),
|
||||
"version_updates": version_updates,
|
||||
"total": len(session.rows),
|
||||
"extracted": len(session.titles),
|
||||
@@ -898,6 +899,8 @@ def create_app(
|
||||
session.decide_manual(row, body.bgg_id)
|
||||
elif body.action == "reject":
|
||||
session.decide_reject(row)
|
||||
elif body.action == "local":
|
||||
session.decide_local(row)
|
||||
else:
|
||||
raise HTTPException(400, f"unknown action {body.action!r}")
|
||||
return state()
|
||||
|
||||
@@ -407,6 +407,14 @@ def test_rpgitem_rows_are_local_only_never_queued():
|
||||
assert result.recognized == 1 # RPGs aren't counted against the queue
|
||||
|
||||
|
||||
def test_local_rows_are_library_citizens_never_queued():
|
||||
local = {**_match("Obscure Homebrew", ""), "match_status": "local"}
|
||||
result = compute_diff([local, _match("Catan", "13")], [])
|
||||
assert result.local_only == ["Obscure Homebrew"]
|
||||
assert [r["bgg_id"] for r in result.to_add] == ["13"]
|
||||
assert result.pending == [] # decided, not waiting
|
||||
|
||||
|
||||
def test_split_copies_become_extra_collection_entries():
|
||||
# three vetoed per-photo copies, one owned: one claims the copy, the
|
||||
# other two are queued as new entries
|
||||
|
||||
@@ -226,3 +226,55 @@ def test_enrich_degrades_without_token(tmp_path, capsys):
|
||||
assert games == {}
|
||||
assert "waiting on the BGG API" in capsys.readouterr().out
|
||||
assert json.loads((cfg.data_dir / "games.json").read_text()) == {}
|
||||
|
||||
|
||||
def test_local_rows_enrich_from_their_own_reads(tmp_path):
|
||||
cfg = Config(data_dir=tmp_path / "data")
|
||||
cfg.data_dir.mkdir(parents=True)
|
||||
write_matches(
|
||||
cfg.matches_path,
|
||||
[
|
||||
{
|
||||
"title_raw": "Obscure Homebrew",
|
||||
"bgg_id": "",
|
||||
"bgg_name": "",
|
||||
"year": "",
|
||||
"type": "",
|
||||
"match_status": "local",
|
||||
"version_id": "",
|
||||
"version_name": "",
|
||||
"version_status": "",
|
||||
"candidates_json": "[]",
|
||||
"version_candidates_json": "[]",
|
||||
"source_photos": "shelf.jpg",
|
||||
}
|
||||
],
|
||||
)
|
||||
cfg.titles_path.write_text(
|
||||
json.dumps(
|
||||
[
|
||||
{
|
||||
"title_raw": "Obscure Homebrew",
|
||||
"publisher_hint": "Basement Press",
|
||||
"year_hint": 1998,
|
||||
"source_photos": ["shelf.jpg"],
|
||||
}
|
||||
]
|
||||
)
|
||||
)
|
||||
from bggpipe.enrich import run_enrich
|
||||
|
||||
client = BGGClient(
|
||||
cache_dir=cfg.data_dir / "cache",
|
||||
transport=httpx.MockTransport(_no_network),
|
||||
)
|
||||
games = run_enrich(cfg, client=client)
|
||||
(key,) = [k for k in games if k.startswith("local:")]
|
||||
entry = games[key]
|
||||
assert entry["name"] == "Obscure Homebrew"
|
||||
assert entry["year"] == 1998
|
||||
assert entry["publishers"] == ["Basement Press"]
|
||||
assert entry["type"] == "localgame"
|
||||
# idempotent: a second run keeps the entry (no prune, no dupe)
|
||||
games2 = run_enrich(cfg, client=client)
|
||||
assert key in games2
|
||||
|
||||
@@ -593,3 +593,15 @@ def test_open_version_ballot_puts_every_edition_up_for_review(tmp_path):
|
||||
)
|
||||
with pytest.raises(ValueError, match="no BGG match"):
|
||||
session2.open_version_ballot(session2.rows[0])
|
||||
|
||||
|
||||
def test_local_decision_keeps_a_real_game_off_bgg(tmp_path):
|
||||
cfg = _setup(
|
||||
tmp_path, [_row(title_raw="Obscure Homebrew", match_status="unmatched")]
|
||||
)
|
||||
run_review(
|
||||
cfg, console=quiet_console(), input_fn=scripted("l"), client=fixture_client()
|
||||
)
|
||||
(row,) = read_matches(cfg.matches_path)
|
||||
assert row["match_status"] == "local"
|
||||
assert row["bgg_id"] == "" # a local citizen carries no BGG identity
|
||||
|
||||
@@ -1128,3 +1128,14 @@ def test_open_versions_endpoint_routes_row_into_review(tmp_path):
|
||||
json={"title_raw": "Dungeon!", "source_photos": "shelf.jpg"},
|
||||
)
|
||||
assert res.status_code == 502
|
||||
|
||||
|
||||
def test_local_decision_via_web(tmp_path):
|
||||
web, cfg = make_client(tmp_path)
|
||||
state = web.post(
|
||||
"/api/decision",
|
||||
json={"title_raw": "Mystery", "source_photos": "shelf.jpg", "action": "local"},
|
||||
).json()
|
||||
saved = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
|
||||
assert saved["Mystery"]["match_status"] == "local"
|
||||
assert state["summary"]["local"] == 1
|
||||
|
||||
Reference in New Issue
Block a user