From 6da499d31aea5c557caf368fdd9ee45bc9318c7c Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sat, 1 Aug 2026 16:45:57 -0400 Subject: [PATCH] Review UI: surface the unresolved backlog and a full catalog ledger The done screen claimed "diff-ready" while 104 extracted titles had never been resolved (they're invisible to matches.csv until the BGG token arrives). The state now counts titles.json entries with no matches row: the header tally shows "awaiting resolve", the done screen says "Resolved set fully reviewed" with the real extracted total and what to run on token day, and a read-only Catalog section lists every extracted title with its status chip (auto/approved/rejected/awaiting BGG), matched game, version, and source photos. Co-Authored-By: Claude Fable 5 --- data/matches.csv | 2 +- data/unidentified_dismissed.json | 54 +++++++++++++++++++++++++++++++ src/bggpipe/review.py | 5 +-- src/bggpipe/templates/review.html | 49 ++++++++++++++++++++++++++-- src/bggpipe/webreview.py | 24 ++++++++++++++ tests/test_webreview.py | 20 +++++++++++- 6 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 data/unidentified_dismissed.json diff --git a/data/matches.csv b/data/matches.csv index 5311786..3f6a4d5 100644 --- a/data/matches.csv +++ b/data/matches.csv @@ -25,4 +25,4 @@ FLAT TOP,2529,Flat Top,1977,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2529 Diplomacy,483,Diplomacy,1959,boardgame,auto,,,version_unknown,"[{""bgg_id"": 483, ""name"": ""Diplomacy"", ""year"": 1959, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg Age of Renaissance,26,Age of Renaissance,1996,boardgame,auto,2601,Avalon Hill English edition,version_auto,"[{""bgg_id"": 26, ""name"": ""Age of Renaissance"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 2601, ""name"": ""Avalon Hill English edition"", ""year"": 1996, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg History of the World,224,History of the World,1991,boardgame,auto,22401,Avalon Hill English edition,version_auto,"[{""bgg_id"": 224, ""name"": ""History of the World"", ""year"": 1991, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 22401, ""name"": ""Avalon Hill English edition"", ""year"": 1993, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg -DUNGEON!,1339,Dungeon!,1975,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 1339, ""name"": ""Dungeon!"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 133901, ""name"": ""TSR first edition"", ""year"": 1975, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133902, ""name"": ""TSR second edition"", ""year"": 1980, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133903, ""name"": ""TSR New Dungeon edition"", ""year"": 1989, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg +DUNGEON!,1339,Dungeon!,1975,boardgame,auto,133903,TSR New Dungeon edition,version_approved,"[{""bgg_id"": 1339, ""name"": ""Dungeon!"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 133901, ""name"": ""TSR first edition"", ""year"": 1975, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133902, ""name"": ""TSR second edition"", ""year"": 1980, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133903, ""name"": ""TSR New Dungeon edition"", ""year"": 1989, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg diff --git a/data/unidentified_dismissed.json b/data/unidentified_dismissed.json new file mode 100644 index 0000000..0c860b8 --- /dev/null +++ b/data/unidentified_dismissed.json @@ -0,0 +1,54 @@ +[ + "IMG_4505.jpeg|Bottom area, left side near Mantis Falls, small figure/box partially visible||Small gold/tan colored object, possibly a game piece or small box, mostly obscured", + "IMG_4505.jpeg|Top shelf, far left edge, partially cut off by frame, to the left of Cat Crimes||Dark brown/maroon box spine with gold decorative swirl design, portion visible at left edge of image", + "IMG_4506.jpeg|Middle area, below and left of Santorini, dark red/maroon box spine with small square icons||Dark reddish-brown spine with small colored square icons in a row, text not legible", + "IMG_4506.jpeg|Right side, background behind Santorini box, partially visible dark box with small game piece icon||Dark background box with a small blue/white icon, mostly obscured by Santorini box in foreground", + "IMG_4506.jpeg|Top shelf, above Santorini spine, partially visible box with patterned tiles/squares in yellow, blue, and cream tones||Patchwork/quilt-like pattern of colored squares (yellow, tan, blue) along the top edge of the box, style suggests a tile-based board game", + "IMG_4506.jpeg|Top shelf, right side near blue corner box, small red 'X' symbols visible on light blue background|X X|Light blue box edge with red X pattern, possibly a tally or scoring game box", + "IMG_4507.jpeg|Middle shelf area, partially hidden behind Labyrinth box and below Utter Nonsense, left side of the stack|Dragon...|Green/dark colored box spine, text cut off by adjacent boxes, only 'Dragon' prefix visible", + "IMG_4507.jpeg|Upper right area near Santorini and a Gentle Rain, appears to be a narrow spine box|PATCH WORK or similar|Colorful patchwork/quilt-like pattern visible on spine, tan and multicolor squares design", + "IMG_4508.jpeg|Top shelf, center, yellow tin box sitting next to the Super Mario Checkers box||Small yellow tin, no visible title text, could be a card game tin", + "IMG_4508.jpeg|Top shelf, far left, behind the stack of papers/notebooks, next to the framed picture||Small black box with unreadable logo, mostly obscured by papers", + "IMG_4508.jpeg|Top shelf, right of center, wooden box with metal clasp between the checkers box and the papers stack||Wooden box with brass hardware, no visible title, appears decorative or a game storage box", + "IMG_4508.jpeg|Top shelf, right side, partially visible box behind the wooden box, between the stack of papers and the edge of the frame|SCRABBLE (partial letters visible)|Red and teal colored box edge, appears to be a Scrabble-branded box but mostly obscured", + "IMG_4511.jpeg|Middle shelf, black box stacked below SUPERFIGHT, above ROOK; left side of shelf|...Go... Yourself|Black spine box, small white/colored text, similar size to Superfight box", + "IMG_4511.jpeg|Right edge of shelf, partially cut off by frame, next to Cthulhu box|HEX|Blue box with yellow/tan geometric pattern, only edge visible", + "IMG_4517.jpeg|Middle of shelf, between Pie Face! (left) and the blue box with bird artwork (right)|...Grenade...|Tall narrow box, dark red/brown color scheme, appears to have a hand grenade or bomb-like illustration", + "IMG_4517.jpeg|Middle of shelf, right of the grenade-themed box and left of Walk the Plank!|Bird ... Bird ...|Blue box with tall bird-like cartoon character illustration, small/narrow box shape", + "IMG_4518.jpeg|Between Mage Wars Academy Core Set and Saberu Merry Men||Thin black spine, largely obscured, possibly a second small game box", + "IMG_4519.jpeg|Far right of shelf, white spine box next to the two red 'G' logo boxes|Before I Kill You, Mister Bond... / Kill Dr. No Lucky|White worn spine with black text, possibly James Bond themed game, 'CHEAPEST GAMES' text also visible on spine which may be a store sticker rather than title", + "IMG_4519.jpeg|Right-center of shelf, two matching red boxes standing between the D&D group on the left and the white 'Cheapest Games' spine on the right|Orange 'G' logo visible on both spines|Two identical red boxes with worn/taped edges, orange circular 'G' emblem, appears to be same game duplicated", + "IMG_4520.jpeg|Middle shelf, between Carcassonne and Evolution: The Beginning||Plain wooden-colored vertical box, no visible text, appears to be a wood grain finish game box or insert", + "IMG_4521.jpeg|Top edge of frame, above the Sleeping Gods box, partially cut off||Dark spines/boxes barely visible at top, too obscured by shadow and cropping to identify", + "IMG_4527.jpeg|Left edge of shelf, partially visible spines to the left of the main game box, on the top shelf|01, T...|Appears to be book-like spines, red and dark covers, stacked vertically, likely not games but partially obscured", + "IMG_4528.jpeg|Top shelf, back corner above the Wiz-War box; only the publisher logo area is visible, partially obscured by other items|SLUGFEST GAMES|Colorful red/orange box art, only top publisher banner visible, rest of box obscured", + "IMG_4529.jpeg|Below the 1000-piece puzzle box, spine showing armored warrior artwork, above the face-out Risk Lord of the Rings box|The Middle-earth|Dark battle scene with armored figures, likely same LOTR-themed game box viewed from spine angle", + "IMG_4529.jpeg|Middle of stack between the puzzle box and Risk Lord of the Rings, tan/cream colored spine with Asian characters and blue castle icon|unreadable Asian characters, blue icon|Tan spine with small blue building/castle graphic and Asian script text", + "IMG_4529.jpeg|Top of stack, second partial box edge, above the puzzle box|0|White/light colored box edge with a circular icon, only corner visible", + "IMG_4529.jpeg|Very top of stack, box behind/above the numbered fragments, purple/dark spine||Dark purple or navy spine, mostly obscured, edge of frame", + "IMG_4529.jpeg|Very top of stack, partial box edge visible above the puzzle box|3|Dark colored spine/edge, only a sliver visible, number '3' printed in white", + "IMG_4532.jpeg|Top right corner of image, partially cut off, above and behind the Gloomhaven box|GLO... (possibly part of another title starting with GLO)|Dark box with fiery orange/red background and large white curved text, only edge visible", + "IMG_4542.jpeg|Right side shelf, below Catan box, next to unidentified black-spined game|T...C...|Teal/orange box with train-like graphic, partially obscured, could be a train-themed game", + "IMG_4542.jpeg|Right side shelf, lower row near 'Catan' box||Dark spine with red accent, text too small/blurry to read", + "IMG_4542.jpeg|Top right shelf, partially hidden behind Dragon Land box|From the Makers of, M|Red box with a large stylized 'M' logo and small icons, likely a Mattel-published card game (possibly UNO), edge cut off by frame", + "IMG_4543.jpeg|right edge of image, behind Scrabble Slam box|CAT|Yellow/orange box edge visible, partial text 'CAT' in bold letters, rest obscured by foreground box", + "IMG_4543.jpeg|top right corner of image, above and behind the main Scrabble Slam box|Dogs Kitchen|Small blue/purple box partially cut off at top edge, illustrated cartoon-style artwork", + "IMG_4544.jpeg|Background shelf behind the Cat Stax box, blurred and out of focus, appears to be a row of game/book boxes||Multiple colorful spines/boxes including orange, white, and dark tones, too blurry to make out any text", + "IMG_4545.jpeg|Top shelf, far right edge, partially cut off by frame border|Ravens...|Yellow/orange box with small logo resembling a bird, likely Ravensburger branding but game title not legible", + "IMG_4545.jpeg|Top shelf, upper right corner, above and behind the Pocket Farkel tin, to the right of 'Bugs in the Kitchen'|DRAGON, land|Colorful fantasy-style box art, orange/red hues, dragon imagery visible, title cut off at top edge of frame", + "IMG_4547.jpeg|Top area, right of the maze-like box, small orange/red box||Small red/orange box, text illegible, largely obscured", + "IMG_4547.jpeg|Top left corner of image, partially visible behind main box|AZE|Blue and green box, appears to be a puzzle or maze-themed game, mostly cut off by frame edge", + "IMG_4547.jpeg|Top right corner of image|Dark T...|Dark blue/black box with white text, appears to be 'Dark T' something, possibly 'Dark Tower' or similar, cut off at frame edge", + "IMG_4547.jpeg|Top right, near 'Dark T' box||Colorful box with red/orange tones, illustration unclear, partially obscured by other boxes", + "IMG_4552.jpeg|Top left corner of frame, behind and above the Dark Cults package, no clearly identified games adjacent due to cropping|'Giv...'|Dark colored box with white/red text fragment, likely a card or board game box mostly out of frame", + "IMG_4552.jpeg|Top right edge of frame, partially obscured behind the plastic-wrapped Dark Cults package; no identifiable neighboring games visible due to cropping|'...ard Game', '-6 Players', 'Yeah, Right.'|White box with black text, appears to be a separate board game box edge cut off by frame, cannot determine title", + "IMG_4553.jpeg|Top right corner of image, partially visible behind/beside the main game, edge of frame||White box with a black silhouette figure, appears to be a card or board game box edge, mostly cut off by frame", + "IMG_4555.jpeg|top left corner of image, above the Kill Doctor Lucky box||Colorful box top edge visible, mostly cropped out of frame", + "IMG_4555.jpeg|top right corner of image, partially visible behind and above the main game box|Z-WAR (or similar, partially cut off)|Dark spine, appears to be a book or game box, mostly obscured by hand holding main item", + "IMG_4556.jpeg|Bottom of frame, left of 'Utter nonsense' box, partially stacked papers/boxes||White/light colored edge visible, mostly obscured by foreground objects", + "IMG_4556.jpeg|Bottom right corner, right of the 'Cards Against' box||Purple/violet box, partially visible, text not legible", + "IMG_4556.jpeg|Far right edge of shelf, beside the 'SANTO' box||Colorful patterned box (patchwork-like quilt design), largely cut off at right frame edge", + "IMG_4556.jpeg|Left side, lower area, below the Ravensburger-labeled box, left of Utter Nonsense box||Green box with dinosaur/reptile illustration, mostly cut off at left frame edge", + "IMG_4556.jpeg|Left side, second row, to the left of the green dinosaur-themed box and below the dark unmarked box|Ravensbu...|Blue box with white text, appears to be a puzzle or game brand box, partially cut off at left edge", + "IMG_4556.jpeg|Top left of shelf, above the 'Ravensburger'-labeled box, left of the dark unmarked box||Dark navy/black box, no legible text visible, top left corner of frame" +] diff --git a/src/bggpipe/review.py b/src/bggpipe/review.py index 5daeccf..8b7bec0 100644 --- a/src/bggpipe/review.py +++ b/src/bggpipe/review.py @@ -60,9 +60,10 @@ class ReviewSession: self.rows = read_matches(cfg.matches_path) self.decisions = 0 try: - self._titles = {e.title_raw: e for e in load_titles(cfg.titles_path)} + self.titles = load_titles(cfg.titles_path) except FileNotFoundError: - self._titles = {} + self.titles = [] + self._titles = {e.title_raw: e for e in self.titles} # -- plumbing ------------------------------------------------------- diff --git a/src/bggpipe/templates/review.html b/src/bggpipe/templates/review.html index 7000a49..1614e19 100644 --- a/src/bggpipe/templates/review.html +++ b/src/bggpipe/templates/review.html @@ -166,6 +166,23 @@ background: var(--felt); color: var(--paper); padding: .3rem .8rem; border-radius: 6px; } + .done .waiting { color: var(--brass-deep); font-weight: 600; } + + /* catalog: read-only status ledger, quieter than decision cards */ + .catalog { background: var(--paper); border-radius: 8px; padding: .4rem 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.3); } + .catalog table { width: 100%; border-collapse: collapse; font-size: .85rem; } + .catalog td { padding: .3rem .5rem; border-top: 1px solid var(--paper-edge); vertical-align: top; } + .catalog tr:first-child td { border-top: none; } + .catalog .t { font-weight: 600; } + .catalog .meta { color: var(--ink-soft); } + .chip { + font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; + border-radius: 999px; padding: .1rem .55rem; white-space: nowrap; + } + .chip.ok { background: #dfe9df; color: var(--approve); } + .chip.wait { background: #f3e7cd; color: var(--brass-deep); } + .chip.no { background: #f0ddd8; color: var(--reject); } + .chip.open { background: #e4e4ef; color: #4c4c78; } @media (max-width: 700px) { .card, .ticket { flex-direction: column; } .shots { flex-basis: auto; } @@ -311,20 +328,29 @@ function render() { `${s.pending.length} matches ${s.versions.length} editions ${s.unidentified.length} reshoot + ${s.summary.unresolved ? `${s.summary.unresolved} awaiting resolve` : ""} ${s.decisions} decided this sitting`; let html = ""; if (!s.pending.length && !s.versions.length) { + const waiting = s.summary.unresolved; html += `
-

All reviewed — this catalog is diff-ready

+

${waiting + ? "Resolved set fully reviewed" + : "All reviewed — this catalog is diff-ready"}

+
${s.summary.extracted}extracted
${s.summary.recognized}recognized
${s.summary.version_updates}with versions
${s.summary.rejected}rejected
-

Next: uv run bggpipe diff

- ${s.unidentified.length ? `

${s.unidentified.length} reshoot ticket(s) below — they don't block the diff.

` : ""} + ${waiting + ? `

${waiting} title(s) extracted but not yet matched to BGG — + they're waiting on the API token. When it arrives: + uv run bggpipe resolve, then review the new arrivals here.

` + : `

Next: uv run bggpipe diff

`} + ${s.unidentified.length ? `

${s.unidentified.length} reshoot ticket(s) below — they don't block anything.

` : ""}
`; } if (s.pending.length) { @@ -339,6 +365,23 @@ function render() { html += `

Reshoot — boxes seen but not identified

`; html += s.unidentified.map(ticket).join(""); } + if (s.catalog.length) { + const chip = c => { + if (c.status === "awaiting_resolve") return `awaiting BGG`; + if (c.status === "auto" || c.status === "approved") return `${c.status}`; + if (c.status === "rejected") return `rejected`; + return `${esc(c.status)}`; + }; + html += `

Catalog — every title extracted so far (${s.catalog.length})

+
` + s.catalog.map(c => ` + + + + + + `).join("") + `
${esc(c.title_raw)}${chip(c)}${c.bgg_name ? esc(c.bgg_name) + (c.bgg_id ? " · " + esc(c.bgg_id) : "") : ""} + ${c.version_name ? " · " + esc(c.version_name) : ""}${esc(c.photos.join(", "))}
`; + } m.innerHTML = html; const cards = actionables(); diff --git a/src/bggpipe/webreview.py b/src/bggpipe/webreview.py index 2a1e12c..3dc87dd 100644 --- a/src/bggpipe/webreview.py +++ b/src/bggpipe/webreview.py @@ -152,6 +152,27 @@ def create_app(cfg: Config, *, client: BGGClient | None = None) -> FastAPI: counts: dict[str, int] = {} for row in session.rows: counts[row["match_status"]] = counts.get(row["match_status"], 0) + 1 + resolved_titles = {r["title_raw"] for r in session.rows} + rows_by_title: dict[str, dict] = {} + for r in session.rows: + rows_by_title.setdefault(r["title_raw"], r) + catalog = [] + for entry in session.titles: + row = rows_by_title.get(entry.title_raw) + catalog.append( + { + "title_raw": entry.title_raw, + "confidence": entry.confidence, + "photos": list(entry.source_photos), + "status": row["match_status"] if row else "awaiting_resolve", + "bgg_id": row["bgg_id"] if row else "", + "bgg_name": row["bgg_name"] if row else "", + "version_name": row["version_name"] if row else "", + } + ) + unresolved_count = sum( + 1 for e in session.titles if e.title_raw not in resolved_titles + ) version_updates = sum( 1 for r in session.rows @@ -172,6 +193,7 @@ def create_app(cfg: Config, *, client: BGGClient | None = None) -> FastAPI: "pending": [row_payload(r) for r in session.pending_rows()], "versions": [version_payload(r) for r in session.version_rows()], "unidentified": sightings, + "catalog": catalog, "decisions": session.decisions, "summary": { "recognized": counts.get("auto", 0) + counts.get("approved", 0), @@ -180,6 +202,8 @@ def create_app(cfg: Config, *, client: BGGClient | None = None) -> FastAPI: "rejected": counts.get("rejected", 0), "version_updates": version_updates, "total": len(session.rows), + "extracted": len(session.titles), + "unresolved": unresolved_count, }, } diff --git a/tests/test_webreview.py b/tests/test_webreview.py index de807fc..492f849 100644 --- a/tests/test_webreview.py +++ b/tests/test_webreview.py @@ -105,7 +105,13 @@ def make_cfg(tmp_path) -> Config: "publisher_hint": "Fantasy Flight", "edition_hint": "", "source_photos": ["shelf.jpg"], - } + }, + { + # extracted but never resolved (no BGG token yet) + "title_raw": "Fresh Off The Shelf", + "confidence": "high", + "source_photos": ["shelf.jpg"], + }, ] ) ) @@ -268,3 +274,15 @@ def test_thumbnails_come_from_cached_thing_xml(tmp_path): ) thumbs = load_thumbnails(cache) assert thumbs == {478: "https://cf.example/citadels.jpg"} + + +def test_catalog_and_unresolved_backlog_are_visible(tmp_path): + """Extracted-but-unresolved titles must not vanish from the UI, and the + done state must not claim diff-readiness while they exist.""" + web, _ = make_client(tmp_path) + state = web.get("/api/state").json() + assert state["summary"]["extracted"] == 2 + assert state["summary"]["unresolved"] == 1 + by_title = {c["title_raw"]: c for c in state["catalog"]} + assert by_title["Fresh Off The Shelf"]["status"] == "awaiting_resolve" + assert by_title["Citadels"]["status"] == "ambiguous"