From 0ed2145f80b3c224ff6fe7b4d3808ac4d5206e6a Mon Sep 17 00:00:00 2001
From: Eric Wagoner
Date: Sun, 9 Aug 2026 16:17:31 -0400
Subject: [PATCH] Where it lives vs where it IS: the away field for loans and
travel
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Eric: this shows where games live, but Frosthaven is in the travel
case right now, and sometimes a game is at a friend's house. A home
and a whereabouts are different facts, so a location record can now
carry away: free text for where the box IS when that isn't home.
Set it in the Right now field on any game's detail page ("travel
case", "loaned to Ben"); blank brings it back and leaves no husk of a
record behind. Home is untouched throughout: the opening assignment
stays, the spot stays reserved in the packing math — a loan is not a
move. Contents ride along the same way they ride on shelves: games
stored inside an out box read as out with it, through whole chains.
The home opening's sheet shows an "out:" chip, unshelved rows show it
too, and the exported game page appends "right now: …" beside where
it lives. Away survives locate edits, lane flips, and CSV re-imports.
Verified live, with Eric's actual fact as the test: Frosthaven marked
out to the travel case from its page — its E1 sheet row now reads
"out: travel case · standing, proud", zero page errors.
Co-Authored-By: Claude Fable 5
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
---
.../page-2026-08-09T20-16-51-813Z.yml | 160 ++++++++++++++++++
.../page-2026-08-09T20-17-09-250Z.yml | 63 +++++++
CLAUDE.md | 2 +-
data/games.json | 2 +-
data/locations.json | 14 +-
data/matches.csv | 2 +-
data/shelf_plan.csv | 1 -
docs/guide.md | 9 +
src/bggpipe/export.py | 10 +-
src/bggpipe/shelves.py | 31 ++++
src/bggpipe/templates/pages/help.html | 2 +-
src/bggpipe/templates/pages/librarygame.html | 20 ++-
src/bggpipe/templates/pages/shelves.html | 4 +
src/bggpipe/webreview.py | 41 ++++-
tests/test_shelves.py | 58 +++++++
15 files changed, 406 insertions(+), 13 deletions(-)
create mode 100644 .playwright-mcp/page-2026-08-09T20-16-51-813Z.yml
create mode 100644 .playwright-mcp/page-2026-08-09T20-17-09-250Z.yml
diff --git a/.playwright-mcp/page-2026-08-09T20-16-51-813Z.yml b/.playwright-mcp/page-2026-08-09T20-16-51-813Z.yml
new file mode 100644
index 0000000..ba63d9f
--- /dev/null
+++ b/.playwright-mcp/page-2026-08-09T20-16-51-813Z.yml
@@ -0,0 +1,160 @@
+- generic [active] [ref=e1]:
+ - link "Skip to content" [ref=e2] [cursor=pointer]:
+ - /url: "#main"
+ - generic [ref=e3]:
+ - link "bggpipe shelf → BGG pipeline" [ref=e4] [cursor=pointer]:
+ - /url: /
+ - generic [ref=e5]:
+ - text: bggpipe
+ - generic [ref=e6]: shelf → BGG pipeline
+ - navigation "Primary" [ref=e7]:
+ - link "Pipeline" [ref=e8] [cursor=pointer]:
+ - /url: /
+ - link "Photos 4" [ref=e9] [cursor=pointer]:
+ - /url: /photos
+ - text: Photos
+ - generic [ref=e10]: "4"
+ - link "Titles" [ref=e11] [cursor=pointer]:
+ - /url: /titles
+ - link "Review" [ref=e12] [cursor=pointer]:
+ - /url: /review
+ - link "Queue" [ref=e13] [cursor=pointer]:
+ - /url: /queue
+ - link "Library" [ref=e14] [cursor=pointer]:
+ - /url: /library
+ - link "Shelves" [ref=e15] [cursor=pointer]:
+ - /url: /shelves
+ - link "Help" [ref=e16] [cursor=pointer]:
+ - /url: /help
+ - figure "art by Juniper" [ref=e17]:
+ - img "the bggpipe piper — a bagpiper whose bag is a board game box" [ref=e18]
+ - generic [ref=e20]: BoardGameGeek and BGG are trademarks of BoardGameGeek, LLC. bggpipe is an independent project, not affiliated with or endorsed by BoardGameGeek.
+ - generic [ref=e21]:
+ - status
+ - main [ref=e22]:
+ - heading "Frosthaven" [level=1] [ref=e23]
+ - generic [ref=e24]:
+ - link "← library" [ref=e25] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e26]:
+ - text: 2022 ·
+ - link "view on BGG ↗" [ref=e27] [cursor=pointer]:
+ - /url: https://boardgamegeek.com/boardgame/295770
+ - generic [ref=e28]:
+ - generic [ref=e29]:
+ - img "box art for Frosthaven" [ref=e31]
+ - generic [ref=e32]:
+ - generic [ref=e33]: players 1–4 players (best at 3)
+ - generic [ref=e34]: playing time 90–180 min
+ - generic [ref=e35]: box 11.811 × 16.1417 × 9.84252 in
+ - generic [ref=e36]: ages 14+
+ - generic [ref=e37]: weight 4.41 / 5
+ - generic [ref=e38]: BGG rank 20
+ - generic [ref=e39]: BGG rating 8.72
+ - generic [ref=e40]: owned by 25,025 people
+ - generic [ref=e41]:
+ - text: designers
+ - generic [ref=e42]: Isaac Childres
+ - generic [ref=e44]:
+ - text: artists
+ - generic [ref=e45]:
+ - generic [ref=e46]: Francesca Baerald
+ - generic [ref=e47]: David Demaret
+ - generic [ref=e48]: Alexandr Elichev
+ - generic [ref=e49]: Josh T. McDowell
+ - generic [ref=e50]:
+ - text: publishers
+ - generic [ref=e51]:
+ - generic [ref=e52]: Cephalofair Games
+ - generic [ref=e53]: Albi
+ - generic [ref=e54]: Albi Polska
+ - generic [ref=e55]: CMON Global Limited
+ - generic [ref=e56]: Delta Vision Publishing
+ - generic [ref=e57]: Feuerland Spiele
+ - generic [ref=e58]:
+ - text: categories
+ - generic [ref=e59]:
+ - generic [ref=e60]: Adventure
+ - generic [ref=e61]: Exploration
+ - generic [ref=e62]: Fantasy
+ - generic [ref=e63]: Fighting
+ - generic [ref=e64]: Miniatures
+ - generic [ref=e65]:
+ - text: mechanics
+ - generic [ref=e66]:
+ - generic [ref=e67]: Campaign / Battle Card Driven
+ - generic [ref=e68]: Communication Limits
+ - generic [ref=e69]: Cooperative Game
+ - generic [ref=e70]: Deck Construction
+ - generic [ref=e71]: Deck, Bag, and Pool Building
+ - generic [ref=e72]: Grid Movement
+ - generic [ref=e73]: Hand Management
+ - generic [ref=e74]: Hexagon Grid
+ - generic [ref=e75]: Legacy Game
+ - generic [ref=e76]: Modular Board
+ - generic [ref=e77]: Multi-Use Cards
+ - generic [ref=e78]: Narrative Choice / Paragraph
+ - generic [ref=e79]: Once-Per-Game Abilities
+ - generic [ref=e80]: Scenario / Mission / Campaign Game
+ - generic [ref=e81]: Simultaneous Action Selection
+ - generic [ref=e82]: Solo / Solitaire Game
+ - generic [ref=e83]: Variable Player Powers
+ - heading "Your edition" [level=2] [ref=e84]
+ - generic [ref=e85]:
+ - generic [ref=e86]: English Kickstarter edition, first printing · 2022
+ - generic [ref=e87]:
+ - text: publishers
+ - generic [ref=e88]: Cephalofair Games
+ - generic [ref=e90]:
+ - text: languages
+ - generic [ref=e91]: English
+ - heading "Where it lives" [level=2] [ref=e93]
+ - generic [ref=e94]:
+ - paragraph [ref=e95]: "Lives: Library Left Shelving · E1"
+ - paragraph [ref=e96]:
+ - text: "Right now: home"
+ - textbox "where it is right now" [ref=e97]:
+ - /placeholder: travel case · loaned to Ben — blank = it's back
+ - button "save" [ref=e98] [cursor=pointer]
+ - text: home stays reserved — this is where the box IS, not where it lives
+ - paragraph [ref=e99]:
+ - combobox "shelf opening" [ref=e100]:
+ - option "nowhere / a note only"
+ - option "Top (fits)"
+ - option "A1 — oversize (fits)"
+ - option "A2 — oversize (fits)"
+ - option "B1 — party (doesn't fit)"
+ - option "B2 — party (doesn't fit)"
+ - option "B3 — party (doesn't fit)"
+ - option "B4 — two-player (doesn't fit)"
+ - option "C1 — general (doesn't fit)"
+ - option "C2 — general (doesn't fit)"
+ - option "C3 — general (doesn't fit)"
+ - option "C4 — general (doesn't fit)"
+ - option "D1 — general (doesn't fit)"
+ - option "D2 — vintage wargames (doesn't fit)"
+ - option "D3 — heavy (doesn't fit)"
+ - option "D4 — campaign (doesn't fit)"
+ - option "Top (doesn't fit)"
+ - option "A1 (doesn't fit)"
+ - option "B1 (doesn't fit)"
+ - option "C1 (doesn't fit)"
+ - option "D1 (doesn't fit)"
+ - option "E1 (doesn't fit)"
+ - option "Top (doesn't fit)"
+ - option "A1 (doesn't fit)"
+ - option "B1 (doesn't fit)"
+ - option "C1 (doesn't fit)"
+ - option "D1 (doesn't fit)"
+ - option "E1 (doesn't fit)" [selected]
+ - textbox "note — lent to Sarah, June" [ref=e101]
+ - button "save" [ref=e102] [cursor=pointer]
+ - paragraph [ref=e103]:
+ - text: In its own box.
+ - button "it lives inside another box…" [ref=e104] [cursor=pointer]
+ - heading "Seen on your shelves" [level=2] [ref=e105]
+ - paragraph [ref=e107]:
+ - link "IMG_4531.jpeg" [ref=e108] [cursor=pointer]:
+ - /url: /photos/view/IMG_4531.jpeg
+ - heading "About" [level=2] [ref=e109]
+ - paragraph [ref=e111]: "Frosthaven is the story of a small outpost far to the north of the capital city of White Oak. It's an outpost barely surviving the harsh weather let alone invasions from forces both known and unknown. However, a group of mercenaries, at the end of their rope, will help bring this settlement back from the edge of destruction. Not only will they have to deal with the harsh elements, but with other, far more dangerous threats out in the unforgiving cold, as well. There are: Algox, the bigger, more yeti-like cousins of the Inox, attacking from the mountains; Lurkers flooding in from the northern sea; and rumors have it that there are machines that wander the frozen wastes of their own free will. The party of mercenaries must face all of these perils, and perhaps in doing so, make peace with these new races so they can work together against even more sinister forces. Frosthaven is a standalone adventure from the designer and publisher of Gloomhaven that features sixteen new characters, three new races, more than twenty new enemies, more than one hundred new items, and a new, 100-scenario campaign. Characters and items from Gloomhaven will be usable in Frosthaven, and vice versa. In addition to using the well-known combat mechanisms of Gloomhaven, Frosthaven features other elements, such as mysteries to solve, a seasonal event system to live through, and player control over how the ramshackle village expands, with each new building offering new ways to progress. Frosthaven has a whole new set of items but there is a mechanism for bringing items over from 'Gloomhaven'. However, as Frosthaven's outpost is a remote location, these products may be imported but are not present as standard items. Resources are much more valuable and you have to build items through a crafting system rather than just buying them. —description from the publisher"
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-08-09T20-17-09-250Z.yml b/.playwright-mcp/page-2026-08-09T20-17-09-250Z.yml
new file mode 100644
index 0000000..20846ef
--- /dev/null
+++ b/.playwright-mcp/page-2026-08-09T20-17-09-250Z.yml
@@ -0,0 +1,63 @@
+- generic [active] [ref=f1e1]:
+ - link "Skip to content" [ref=f1e2] [cursor=pointer]:
+ - /url: "#main"
+ - generic [ref=f1e3]:
+ - link "bggpipe shelf → BGG pipeline" [ref=f1e4] [cursor=pointer]:
+ - /url: /
+ - generic [ref=f1e5]:
+ - text: bggpipe
+ - generic [ref=f1e6]: shelf → BGG pipeline
+ - navigation "Primary" [ref=f1e7]:
+ - link "Pipeline" [ref=f1e8] [cursor=pointer]:
+ - /url: /
+ - link "Photos" [ref=f1e9] [cursor=pointer]:
+ - /url: /photos
+ - link "Titles" [ref=f1e10] [cursor=pointer]:
+ - /url: /titles
+ - link "Review" [ref=f1e11] [cursor=pointer]:
+ - /url: /review
+ - link "Queue" [ref=f1e12] [cursor=pointer]:
+ - /url: /queue
+ - link "Library" [ref=f1e13] [cursor=pointer]:
+ - /url: /library
+ - link "Shelves" [ref=f1e14] [cursor=pointer]:
+ - /url: /shelves
+ - link "Help" [ref=f1e15] [cursor=pointer]:
+ - /url: /help
+ - figure "art by Juniper" [ref=f1e16]:
+ - img "the bggpipe piper — a bagpiper whose bag is a board game box" [ref=f1e17]
+ - generic [ref=f1e19]: BoardGameGeek and BGG are trademarks of BoardGameGeek, LLC. bggpipe is an independent project, not affiliated with or endorsed by BoardGameGeek.
+ - generic [ref=f1e20]:
+ - status
+ - main [ref=f1e21]:
+ - heading "Shelves" [level=1] [ref=f1e22]
+ - heading "Add a unit" [level=2] [ref=f1e23]
+ - generic [ref=f1e24]:
+ - generic [ref=f1e25]:
+ - text: Unit name
+ - textbox "Unit name" [ref=f1e26]:
+ - /placeholder: Living room Kallax
+ - generic [ref=f1e27]:
+ - text: Preset
+ - combobox "Preset" [ref=f1e28]:
+ - option "IKEA Kallax cube (13.25 × 13.25 × 15.4)" [selected]
+ - option "IKEA Billy shelf (30.75 × 13 × 11)"
+ - option "custom size"
+ - option "no size — travel case, lent out…"
+ - generic [ref=f1e29]:
+ - text: Rows
+ - textbox "Rows" [ref=f1e30]: "1"
+ - generic [ref=f1e31]:
+ - text: Columns
+ - textbox "Columns" [ref=f1e32]: "1"
+ - generic [ref=f1e33]:
+ - text: Zone
+ - textbox "Zone" [ref=f1e34]:
+ - /placeholder: party games
+ - generic [ref=f1e35]:
+ - text: Description
+ - textbox "Description" [ref=f1e36]:
+ - /placeholder: the wall behind the couch — main game storage
+ - button "create" [ref=f1e38] [cursor=pointer]
+ - generic [ref=f1e39]: grid mode labels openings A1, A2… by row; you can edit, add, or remove openings afterward — repeat with another grid to stack sections (double-wides above cubes, say) in the same unit
+ - paragraph [ref=f1e41]: Loading…
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index a81c654..35318f6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -50,7 +50,7 @@ Full design lives in `docs/spec.md` (read it before changing pipeline semantics)
- Base game vs. expansion vs. new edition is the top failure mode — bias matching toward `ambiguous` over auto-match ("Wingspan Europe" must not match base Wingspan).
- Editions/versions matter: Eric owns multiple editions of some games — each is a separate collection entry (keyed by `collid` on BGG). Never guess a version: no legible cues → `version_unknown` and a version-less collection entry.
- Normalize titles (casefold, strip punctuation/articles, special chars like é/&/:) identically on both sides of a match; dedupe across photos but keep `source_photos` provenance.
-- **Human curation is durable**: `data/title_splits.json` (photo-scoped split-into-copies decisions, honored by extract's dedupe AND resolve's dedupe), `data/title_edits.json` (corrected reads/cues, applied before dedupe on every titles.json rebuild), `data/title_removals.json` (lines removed from the catalog — filtered out of every rebuild; delete the record to undo), `data/title_additions.json` (games added without a photo — joined into every rebuild; a later photo sighting dedupe-merges with them), `data/local_games.json` + `data/local_art/` (hand-written facts and a cover photo for off-BGG games — the ONLY source for them, merged over the photo reads by enrich), and `data/furniture.json` + `data/locations.json` (the owner's shelf layout and each game's physical spot — local-only, never BGG's business; a location can carry `lane` and `acknowledged` — the human overruling the packer), and `data/measurements.json` (hand-measured box dims, source `measured` — the owner's tape measure outranks BGG, overlaid on every games.json read) persist forever. Row-level decisions persist via the `dedupe_veto` column — edits never drop veto'd rows (a rename retitles them in place); removal drops them (explicitly discarding the line).
+- **Human curation is durable**: `data/title_splits.json` (photo-scoped split-into-copies decisions, honored by extract's dedupe AND resolve's dedupe), `data/title_edits.json` (corrected reads/cues, applied before dedupe on every titles.json rebuild), `data/title_removals.json` (lines removed from the catalog — filtered out of every rebuild; delete the record to undo), `data/title_additions.json` (games added without a photo — joined into every rebuild; a later photo sighting dedupe-merges with them), `data/local_games.json` + `data/local_art/` (hand-written facts and a cover photo for off-BGG games — the ONLY source for them, merged over the photo reads by enrich), and `data/furniture.json` + `data/locations.json` (the owner's shelf layout and each game's physical spot — local-only, never BGG's business; a location can carry `lane`, `acknowledged` — the human overruling the packer — and `away`, where the box IS right now (loans/travel; home stays reserved, contents inherit)), and `data/measurements.json` (hand-measured box dims, source `measured` — the owner's tape measure outranks BGG, overlaid on every games.json read) persist forever. Row-level decisions persist via the `dedupe_veto` column — edits never drop veto'd rows (a rename retitles them in place); removal drops them (explicitly discarding the line).
- **RPGs are local-only citizens**: when the board-game search runs dry, resolve falls back to `type=rpgitem` (same geekdo API/token). RPGGeek items carry their OWN link types (`rpgdesigner`, `rpgpublisher`, `rpggenre`, `rpgcategory`, `rpgmechanic`) — a board-game-only parser silently returns nothing for them. Matched rpgitems enrich into the library but diff routes them to `local_only` — they must never reach `to_add.csv`/upload (their collection lives on RPGGeek, out of scope).
- Detailed BGG API behavior (202 queueing, collection-endpoint quirks, endpoints): use the `bgg-api` skill. **If the spec's BGG behavior changes, update the `bgg-api` skill to match** — they must not drift.
diff --git a/data/games.json b/data/games.json
index c0b90c6..fe947b5 100644
--- a/data/games.json
+++ b/data/games.json
@@ -9698,7 +9698,7 @@
"weight_lb": null,
"source": "absent"
},
- "stored_in": null
+ "stored_in": "432858"
},
"104590:117256": {
"bgg_id": 104590,
diff --git a/data/locations.json b/data/locations.json
index e9f2693..e3db7b6 100644
--- a/data/locations.json
+++ b/data/locations.json
@@ -109,6 +109,7 @@
"opening_id": "library-kallax-c4"
},
"166109:253058": {
+ "acknowledged": true,
"note": "",
"opening_id": "library-kallax-b1"
},
@@ -306,6 +307,7 @@
"opening_id": "library-kallax-d4"
},
"290840:474978": {
+ "acknowledged": true,
"note": "",
"opening_id": "library-right-shelving-e1"
},
@@ -315,6 +317,7 @@
"opening_id": "library-right-shelving-e1"
},
"291131:475388": {
+ "acknowledged": true,
"note": "",
"opening_id": "library-right-shelving-e1"
},
@@ -326,6 +329,12 @@
"note": "",
"opening_id": "library-kallax-c3"
},
+ "295770:484007": {
+ "acknowledged": true,
+ "away": "travel case",
+ "note": "",
+ "opening_id": "library-right-shelving-2-e1"
+ },
"2980:138013": {
"note": "",
"opening_id": "library-kallax-d1"
@@ -400,6 +409,7 @@
"opening_id": "library-kallax-c4"
},
"358880:603694": {
+ "acknowledged": true,
"note": "",
"opening_id": "library-right-shelving-e1"
},
@@ -463,10 +473,6 @@
"note": "",
"opening_id": "library-kallax-c2"
},
- "452833:770724": {
- "note": "",
- "opening_id": "library-kallax-c1"
- },
"453845:772660": {
"note": "",
"opening_id": "library-kallax-c3"
diff --git a/data/matches.csv b/data/matches.csv
index 2897835..e42fa7d 100644
--- a/data/matches.csv
+++ b/data/matches.csv
@@ -132,7 +132,7 @@ MAGE WARS ACADEMY,172503,Mage Wars Academy,2015,boardgame,auto,265107,English ed
BOTANY: TANTALIZING TREES,417060,Botany: Tantalizing Trees,2024,boardgameexpansion,auto,706506,English edition,version_auto,"[{""bgg_id"": 417060, ""name"": ""Botany: Tantalizing Trees"", ""year"": 2024, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 706506, ""name"": ""English edition"", ""year"": 2024, ""publishers"": [""Dux Somnium Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4571.jpeg,,,380837
BOTANY: PERILOUS PERFUMES,417064,Botany: Perilous Perfumes,2024,boardgameexpansion,auto,706510,English edition,version_auto,"[{""bgg_id"": 417064, ""name"": ""Botany: Perilous Perfumes"", ""year"": 2024, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 706510, ""name"": ""English edition"", ""year"": 2024, ""publishers"": [""Dux Somnium Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4571.jpeg,,,380837
Civilization: West Extension Map,2058,Civilization: West Extension Map,1986,boardgameexpansion,auto,28041,Avalon Hill edition,version_approved,"[{""bgg_id"": 2058, ""name"": ""Civilization: West Extension Map"", ""year"": 1986, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 293811, ""name"": ""Hartland Trefoil edition"", ""year"": 1986, ""publishers"": [""Hartland Trefoil Ltd.""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 28041, ""name"": ""Avalon Hill edition"", ""year"": 1988, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 67095, ""name"": ""German edition"", ""year"": 1992, ""publishers"": [""Welt der Spiele""], ""languages"": [""German""], ""score"": 0}]",,,,
-Artistry: Delightful Doorways Mini Expansion,452833,Artistry: Delightful Doorways Mini Expansion,2025,boardgameexpansion,auto,770724,English edition,version_approved,"[{""bgg_id"": 452833, ""name"": ""Artistry: Delightful Doorways Mini Expansion"", ""year"": 2025, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 770724, ""name"": ""English edition"", ""year"": 2025, ""publishers"": [""Dux Somnium Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 770830, ""name"": ""English Jewelry Box edition"", ""year"": 2025, ""publishers"": [""Dux Somnium Games""], ""languages"": [""English""], ""score"": 0}]",,,,
+Artistry: Delightful Doorways Mini Expansion,452833,Artistry: Delightful Doorways Mini Expansion,2025,boardgameexpansion,auto,770724,English edition,version_approved,"[{""bgg_id"": 452833, ""name"": ""Artistry: Delightful Doorways Mini Expansion"", ""year"": 2025, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 770724, ""name"": ""English edition"", ""year"": 2025, ""publishers"": [""Dux Somnium Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 770830, ""name"": ""English Jewelry Box edition"", ""year"": 2025, ""publishers"": [""Dux Somnium Games""], ""languages"": [""English""], ""score"": 0}]",,,,432858
castle panic wizard's tower,104590,Castle Panic: The Wizard's Tower,2011,boardgameexpansion,auto,117256,"English first edition, first printing",version_approved,"[{""bgg_id"": 104590, ""name"": ""Castle Panic: The Wizard's Tower"", ""year"": 2011, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 117256, ""name"": ""English first edition, first printing"", ""year"": 2011, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 193172, ""name"": ""First edition, second printing"", ""year"": 2012, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 228207, ""name"": ""First edition, fourth printing"", ""year"": 2013, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 208993, ""name"": ""First edition, third printing"", ""year"": 2013, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 280403, ""name"": ""First edition, fifth printing"", ""year"": 2014, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 302898, ""name"": ""First edition, sixth printing"", ""year"": 2015, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 371339, ""name"": ""First edition, seventh printing"", ""year"": 2016, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 476666, ""name"": ""First edition, ninth printing"", ""year"": 2019, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 553422, ""name"": ""First edition, first printing"", ""year"": 2021, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 624504, ""name"": ""English second edition"", ""year"": 2023, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 0}]",,,,
WIZ-WAR,104710,Wiz-War (Eighth Edition),2012,boardgame,approved,117685,English first edition,version_auto,"[{""bgg_id"": 589, ""name"": ""Wiz-War"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 2076, ""rank"": 3049}, {""bgg_id"": 324937, ""name"": ""Wiz-War (9th Edition)"", ""year"": 2023, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 53.8, ""owned"": 482, ""rank"": 11443}, {""bgg_id"": 104710, ""name"": ""Wiz-War (Eighth Edition)"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 48.3, ""owned"": 7742, ""rank"": 1266}]","[{""version_id"": 117685, ""name"": ""English first edition"", ""year"": 2012, ""publishers"": [""Fantasy Flight Games""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 244745, ""name"": ""English second edition"", ""year"": 2013, ""publishers"": [""Fantasy Flight Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 185192, ""name"": ""French edition"", ""year"": 2012, ""publishers"": [""Edge Entertainment""], ""languages"": [""French""], ""score"": 2}, {""version_id"": 188887, ""name"": ""Italian edition"", ""year"": 2012, ""publishers"": [""Giochi Uniti""], ""languages"": [""Italian""], ""score"": 2}, {""version_id"": 164985, ""name"": ""Spanish edition"", ""year"": 2012, ""publishers"": [""Edge Entertainment""], ""languages"": [""Spanish""], ""score"": 2}, {""version_id"": 166902, ""name"": ""Wargames Club Traditional Chinese edition"", ""year"": 2012, ""publishers"": [""Wargames Club Publishing""], ""languages"": [""Chinese""], ""score"": 2}]",IMG_4504.jpeg,,,
The Red Dragon Inn 5: The Character Trove,173634,The Red Dragon Inn 5: The Character Trove,2015,boardgame,approved,266954,English first edition,version_approved,[],"[{""version_id"": 266954, ""name"": ""English first edition"", ""year"": 2015, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 418968, ""name"": ""English second edition"", ""year"": 2016, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 501467, ""name"": ""Russian edition"", ""year"": 2020, ""publishers"": [""Hobby World"", ""SlugFest Games""], ""languages"": [""Russian""], ""score"": 0}, {""version_id"": 728048, ""name"": ""English fifth edition"", ""year"": 2021, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 629264, ""name"": ""English sixth edition"", ""year"": 2021, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 807834, ""name"": ""English edition 2025"", ""year"": 2025, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}]",,,,
diff --git a/data/shelf_plan.csv b/data/shelf_plan.csv
index 67a8537..351a9e6 100644
--- a/data/shelf_plan.csv
+++ b/data/shelf_plan.csv
@@ -49,7 +49,6 @@ Patchwork,library-kallax-b4,
5-Minute Dungeon,library-kallax-c1,
A Gentle Rain,library-kallax-c1,
Artistry,library-kallax-c1,
-Artistry: Delightful Doorways Mini Expansion,library-kallax-c1,
Blink,library-kallax-c1,
Botany,library-kallax-c1,
Calico,library-kallax-c1,
diff --git a/docs/guide.md b/docs/guide.md
index 41daa2f..2451cf2 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -176,6 +176,15 @@ really there, outside the capacity bars, never warned about. Tap
a game listed in an opening IS there regardless of what the packer
thinks of the arrangement.
+Where a game *lives* and where it *is* can differ — the travel case,
+a friend's house. Every game's detail page has a **Right now** field:
+type "travel case" or "loaned to Ben" and the box is marked out
+everywhere (an "out:" chip in its home opening, on the export's game
+page too) while its home spot stays reserved — the shelf math doesn't
+change because a loan isn't a move. Contents ride along: games stored
+inside an out box read as out with it. Blank the field and it's back;
+nothing else is touched.
+
Unmeasured games can't join the capacity math at all — so measure
them. The "unmeasured — measure it" button in the opening's view takes
width × length × depth in inches, saved to `data/measurements.json`
diff --git a/src/bggpipe/export.py b/src/bggpipe/export.py
index 65c51dc..346512d 100644
--- a/src/bggpipe/export.py
+++ b/src/bggpipe/export.py
@@ -28,6 +28,7 @@ from bggpipe.config import Config
from bggpipe.fsio import atomic_write_bytes, atomic_write_text
from bggpipe.shelves import (
apply_measurements,
+ effective_away,
effective_location,
load_furniture,
load_locations,
@@ -637,9 +638,16 @@ def run_export(
cover = art.get(key, {}).get("full", "")
extras = ""
if where.get(key):
+ away = effective_away(key, game, games, locations)
extras += (
'
where it lives '
- f"{html.escape(where[key])}
"
+ f"{html.escape(where[key])}"
+ + (
+ f' — right now: {html.escape(away)}'
+ if away
+ else ""
+ )
+ + ""
)
container_key = next(
(
diff --git a/src/bggpipe/shelves.py b/src/bggpipe/shelves.py
index 7add745..aee1dce 100644
--- a/src/bggpipe/shelves.py
+++ b/src/bggpipe/shelves.py
@@ -179,6 +179,34 @@ def stack_thickness(entry: dict) -> float | None:
# -- the resolved picture ------------------------------------------------
+def effective_away(
+ key: str, entry: dict, games: dict[str, dict], locations: dict[str, dict]
+) -> str:
+ """Where the box IS right now, when that isn't home: its own away
+ note, else the nearest container's — contents ride along on trips
+ the same way they ride along on shelves. "" = at home."""
+ visited = {key}
+ current_key, current = key, entry
+ while True:
+ away = (locations.get(current_key) or {}).get("away", "")
+ if away:
+ return away
+ if not current.get("stored_in"):
+ return ""
+ container = next(
+ (
+ (k, e)
+ for k, e in games.items()
+ if str(e.get("bgg_id")) == str(current["stored_in"])
+ ),
+ None,
+ )
+ if container is None or container[0] in visited:
+ return ""
+ current_key, current = container
+ visited.add(current_key)
+
+
def effective_location(
key: str, entry: dict, games: dict[str, dict], locations: dict[str, dict]
) -> tuple[str | None, str, str | None]:
@@ -497,6 +525,7 @@ def import_assignments(cfg: Config, csv_path: Path) -> dict:
prev = locations.get(key) or {}
# an overhang acknowledged once (in the UI or a prior import)
# stays acknowledged — a plan file needn't re-state it
+ # (and being out on loan doesn't change where home is)
already = bool(prev.get("acknowledged")) and prev.get("opening_id") == oid
if fits_opening(games[key], openings[oid]) is False and not (
acknowledge or already
@@ -511,6 +540,8 @@ def import_assignments(cfg: Config, csv_path: Path) -> dict:
# the note always survives; the lane choice and a standing
# acknowledgment survive as long as the opening is unchanged
record = {"opening_id": oid, "note": prev.get("note", "")}
+ if prev.get("away"):
+ record["away"] = prev["away"]
if prev.get("opening_id") == oid:
if prev.get("lane"):
record["lane"] = prev["lane"]
diff --git a/src/bggpipe/templates/pages/help.html b/src/bggpipe/templates/pages/help.html
index c72ea35..c7b8586 100644
--- a/src/bggpipe/templates/pages/help.html
+++ b/src/bggpipe/templates/pages/help.html
@@ -30,7 +30,7 @@
Titles — every read off your shelves, alphabetized, with its status and photos. This is the proofread checkpoint: edit, split, remove. Its badge counts shaky read lines — the model wasn't sure and nothing has verified them; filter to them, then press ✓ looks right or edit each one.
Review — 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 (keep locally: it joins the Library, never uploads). Keyboard-first; see shortcuts.
Queue — 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. A job that fails is skipped by later runs (so one broken game can't loop forever); when any exist, the Pipeline's upload card offers a retry N failed checkbox. Each queued row shows what upload did with it — pending, done, failed, or retired (a review decision since the last diff withdrew it). Finished rows stay listed until the next diff rebuilds the queue; the log below them is the permanent record.
-
Shelves — where everything physically lives, drawn as it really stands: openings group into rows by label letter with widths proportional to their interiors, so the page is a diagram of your wall. Describe furniture once (presets for Kallax cubes and Billy shelves, grids of rows × columns — a later grid continues the row letters — custom sizes, and no-size spots like a travel case); units and openings both take a free-text description beside the zone keyword. Suggestions rank an opening holding a series-mate or base game first (marked ♥ — every placement you confirm teaches the suggester your organization), always against the remaining room, then tightest verified fit; a measured game with nowhere suitable says "no matching openings yet", and unmeasured games get no suggestions because can't-verify ≠ fits. Assign by tapping suggestions, searching inside an opening, or from any game's detail page. Capacity is a two-lane model — a flat stack against interior height plus a standing lane against the remaining width; the largest boxes lie flat, smaller ones stand, and each box's lane is flippable in the opening's view. ⚠ means no packing fits, not merely "tall stack" — and the packer is deliberately conservative (one flat pile, one standing row) while real shelves are 3-D. When it refuses a placement you can see with your own eyes, the it's here anyway button overrules it: the box takes a lane where one works (spine-out overhangs land "standing, proud") and otherwise is marked wedged in — really there, outside the capacity bars, never warned about. Unmeasured boxes get an unmeasured — measure it button: enter your own tape-measure numbers and they outrank BGG's everywhere sizes are used. Games stored inside another box ride along with their container, through whole chains. Your layout is local data only — never sent anywhere. Bulk-load a plan with bggpipe shelve --import plan.csv (name,opening, plus an optional acknowledge column for deliberate overhangs) — re-imports are upserts that keep notes, lanes, and acknowledgments — and check capacity anytime with bggpipe dims.
+
Shelves — where everything physically lives, drawn as it really stands: openings group into rows by label letter with widths proportional to their interiors, so the page is a diagram of your wall. Describe furniture once (presets for Kallax cubes and Billy shelves, grids of rows × columns — a later grid continues the row letters — custom sizes, and no-size spots like a travel case); units and openings both take a free-text description beside the zone keyword. Suggestions rank an opening holding a series-mate or base game first (marked ♥ — every placement you confirm teaches the suggester your organization), always against the remaining room, then tightest verified fit; a measured game with nowhere suitable says "no matching openings yet", and unmeasured games get no suggestions because can't-verify ≠ fits. Assign by tapping suggestions, searching inside an opening, or from any game's detail page. Capacity is a two-lane model — a flat stack against interior height plus a standing lane against the remaining width; the largest boxes lie flat, smaller ones stand, and each box's lane is flippable in the opening's view. ⚠ means no packing fits, not merely "tall stack" — and the packer is deliberately conservative (one flat pile, one standing row) while real shelves are 3-D. When it refuses a placement you can see with your own eyes, the it's here anyway button overrules it: the box takes a lane where one works (spine-out overhangs land "standing, proud") and otherwise is marked wedged in — really there, outside the capacity bars, never warned about. Unmeasured boxes get an unmeasured — measure it button: enter your own tape-measure numbers and they outrank BGG's everywhere sizes are used. And where a game lives vs where it is can differ: the Right now field on any game's detail page marks a box out (travel case, loaned to a friend) without touching its home — the spot stays reserved, contents ride along, and blanking the field brings it back. Games stored inside another box ride along with their container, through whole chains. Your layout is local data only — never sent anywhere. Bulk-load a plan with bggpipe shelve --import plan.csv (name,opening, plus an optional acknowledge column for deliberate overhangs) — re-imports are upserts that keep notes, lanes, and acknowledgments — and check capacity anytime with bggpipe dims.
Library — your enriched collection. Search titles, designers, mechanics and categories at once; filter by kind (board games, RPGs, off-BGG) or by how many people are playing tonight; sort by name, year, BGG rank, weight, or playing time. Click any game for its full detail: art, the usual stats, designers and mechanics, your edition, the shelf photos it was read from, and a link to its BGG page. RPG and off-BGG games live here too — identified and enriched, never uploaded. RPGs pull their designers, publishers and genres from RPGGeek; an off-BGG game's detail page lets you write its facts yourself and add a cover photo, since nothing else will ever have them (both are saved under data/ and folded in by the next enrich).
+ Right now:
+ ${loc.away ? `${esc(loc.away)}` : `home${loc.away_own === undefined ? "" : ""}`}
+
+
+ home stays reserved — this is where the box IS,
+ not where it lives
+
`;
const locBlock = g.stored_in_game
? `
It rides with its box: see
${esc(g.stored_in_game.name)}${loc.text && !loc.text.startsWith("inside")
- ? ` — currently ${esc(loc.text)}` : ""}.
`
+ ? ` — currently ${esc(loc.text)}` : ""}${loc.away
+ ? ` — right now: ${esc(loc.away)}` : ""}.