Where it lives vs where it IS: the away field for loans and travel

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-09 16:17:31 -04:00
co-authored by Claude Fable 5
parent 6c95953dc0
commit 0ed2145f80
15 changed files with 406 additions and 13 deletions
@@ -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 14 players (best at 3)
- generic [ref=e34]: playing time 90180 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"
@@ -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…
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -9698,7 +9698,7 @@
"weight_lb": null,
"source": "absent"
},
"stored_in": null
"stored_in": "432858"
},
"104590:117256": {
"bgg_id": 104590,
+10 -4
View File
@@ -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"
+1 -1
View File
@@ -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}]",,,,
1 title_raw bgg_id bgg_name year type match_status version_id version_name version_status candidates_json version_candidates_json source_photos merged_into dedupe_veto stored_in
132 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
133 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
134 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}]
135 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
136 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}]
137 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
138 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}]
-1
View File
@@ -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,
1 name opening acknowledge
49 5-Minute Dungeon library-kallax-c1
50 A Gentle Rain library-kallax-c1
51 Artistry library-kallax-c1
Artistry: Delightful Doorways Mini Expansion library-kallax-c1
52 Blink library-kallax-c1
53 Botany library-kallax-c1
54 Calico library-kallax-c1
+9
View File
@@ -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`
+9 -1
View File
@@ -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 += (
'<div class="card"><div class="factrow"><b>where it lives</b> '
f"{html.escape(where[key])}</div></div>"
f"{html.escape(where[key])}"
+ (
f' <span class="meta">— right now: {html.escape(away)}</span>'
if away
else ""
)
+ "</div></div>"
)
container_key = next(
(
+31
View File
@@ -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"]
+1 -1
View File
@@ -30,7 +30,7 @@
<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, 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. 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 <b>retry N failed</b> checkbox. Each queued row shows what upload did with it — <span class="chip open">pending</span>, <span class="chip ok">done</span>, <span class="chip no">failed</span>, or <span class="chip no">retired</span> (a review decision since the last diff withdrew it). Finished rows stay listed until the next <b>diff</b> rebuilds the queue; the log below them is the permanent record.</p>
<p><b><a href="/shelves">Shelves</a></b> — 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 <b>series-mate or base game first</b> (marked ♥ — every placement you confirm teaches the suggester your organization), always against the <i>remaining</i> 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 <i>no packing fits</i>, 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 <b>it's here anyway</b> button overrules it: the box takes a lane where one works (spine-out overhangs land "standing, proud") and otherwise is marked <b>wedged in</b> — really there, outside the capacity bars, never warned about. Unmeasured boxes get an <b>unmeasured — measure it</b> 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 <code>bggpipe shelve --import plan.csv</code> (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 <code>bggpipe dims</code>.</p>
<p><b><a href="/shelves">Shelves</a></b> — 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 <b>series-mate or base game first</b> (marked ♥ — every placement you confirm teaches the suggester your organization), always against the <i>remaining</i> 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 <i>no packing fits</i>, 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 <b>it's here anyway</b> button overrules it: the box takes a lane where one works (spine-out overhangs land "standing, proud") and otherwise is marked <b>wedged in</b> — really there, outside the capacity bars, never warned about. Unmeasured boxes get an <b>unmeasured — measure it</b> button: enter your own tape-measure numbers and they outrank BGG's everywhere sizes are used. And where a game <i>lives</i> vs where it <i>is</i> can differ: the <b>Right now</b> 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 <code>bggpipe shelve --import plan.csv</code> (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 <code>bggpipe dims</code>.</p>
<p><b><a href="/library">Library</a></b> — 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, <b>your</b> 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 <code>data/</code> and folded in by the next <b>enrich</b>).</p>
</div>
+18 -2
View File
@@ -79,6 +79,11 @@ function wireLocation(g) {
});
if (res) { showToast("shelved"); refresh(); }
});
document.getElementById("awaysave")?.addEventListener("click", async () => {
const where = document.getElementById("awayinput").value.trim();
const res = await apiPost("/api/away", {key: KEY, where});
if (res) { showToast(where ? "marked out" : "welcome home"); refresh(); }
});
}
function wireStored(g) {
@@ -215,15 +220,26 @@ function render(g) {
${esc(o.label)}${o.zone ? `${esc(o.zone)}` : ""}
${o.virtual ? "" : ` (${fitWord(o.fit)})`}
</option>`).join("")}</optgroup>`).join("");
const awayRow = `<p id="awayform">
Right now:
${loc.away ? `<b>${esc(loc.away)}</b>` : `<span class="meta">home${loc.away_own === undefined ? "" : ""}</span>`}
<input id="awayinput" placeholder="travel case · loaned to Ben — blank = it's back"
value="${esc(loc.away_own || "")}" aria-label="where it is right now">
<button id="awaysave">save</button>
<span class="edithint">home stays reserved — this is where the box IS,
not where it lives</span>
</p>`;
const locBlock = g.stored_in_game
? `<p>It rides with its box: see
<a href="/library/game/${encodeURIComponent(g.stored_in_game.key)}">
${esc(g.stored_in_game.name)}</a>${loc.text && !loc.text.startsWith("inside")
? ` — currently ${esc(loc.text)}` : ""}.</p>`
? ` — currently ${esc(loc.text)}` : ""}${loc.away
? ` — right now: <b>${esc(loc.away)}</b>` : ""}.</p>${awayRow}`
: `<p>${loc.text
? `Currently: <b>${esc(loc.text)}</b>${loc.note && loc.text !== loc.note
? `Lives: <b>${esc(loc.text)}</b>${loc.note && loc.text !== loc.note
? ` <span class="meta">(${esc(loc.note)})</span>` : ""}`
: `<span class="meta">Unshelved.</span>`}</p>
${awayRow}
<p id="locform">
<select id="locpick" aria-label="shelf opening">
<option value="">nowhere / a note only</option>${menu}
+4
View File
@@ -193,6 +193,8 @@ function unshelvedRow(g) {
|| (g.measured ? `<span class="meta">no matching openings yet</span>` : "");
return `<div class="unshelved-row">
<a href="/library/game/${encodeURIComponent(g.key)}">${esc(g.name)}</a>${dims}
${g.away ? ` <span class="chip open" title="no home shelf, and the box is
elsewhere right now">out: ${esc(g.away)}</span>` : ""}
${g.lost_home ? `<span class="chip no"
title="its assigned opening no longer exists — the shelves changed">shelf gone</span>` : ""}
${g.note ? `<span class="meta">${esc(g.note)}</span>` : ""}
@@ -232,6 +234,8 @@ function sheetRender() {
<div class="sheetrow">
${g.thumbnail ? `<img src="${esc(g.thumbnail)}" alt="">` : ""}
<a href="/library/game/${encodeURIComponent(g.key)}">${esc(g.name)}</a>
${g.away ? `<span class="chip open" title="this is its home spot — the box
itself is elsewhere right now (set from the game's page)">out: ${esc(g.away)}</span>` : ""}
${g.measured ? "" : `<button class="measurebtn" data-key="${esc(g.key)}"
title="no trusted size — tap to enter your own tape-measure numbers
(they outrank BGG's)">unmeasured — measure it</button>`}
+40 -1
View File
@@ -65,6 +65,7 @@ from bggpipe.resolve import MATCH_COLUMNS, TitleEntry, read_matches, write_match
from bggpipe.review import _BGG_ERRORS, ReviewSession
from bggpipe.shelves import (
apply_measurements,
effective_away,
effective_location,
fits_opening,
game_dims,
@@ -283,6 +284,11 @@ class MeasureBody(BaseModel):
depth_in: float | None = None
class AwayBody(BaseModel):
key: str
where: str = "" # "travel case", "loaned to Ben"; "" = back home
class AcknowledgeBody(BaseModel):
key: str
acknowledged: bool # True = "doesn't fit, shelved anyway" is deliberate
@@ -990,6 +996,8 @@ def create_app(
"note": note,
"via": via,
"text": where,
"away": effective_away(key, game, games, locations),
"away_own": (locations.get(key) or {}).get("away", ""),
},
}
return out
@@ -1145,6 +1153,7 @@ def create_app(
"acknowledged": k in acked,
"wedged": (e.get("name") or "?") in report["wedged"],
"proud": (e.get("name") or "?") in report["proud"],
"away": effective_away(k, e, games, locations),
}
for k, e in sorted(
here,
@@ -1208,6 +1217,7 @@ def create_app(
"name": entry.get("name"),
"thumbnail": entry.get("thumbnail") or "",
"note": note,
"away": effective_away(key, entry, games, locations),
"measured": game_dims(entry) is not None,
"lost_home": key in lost_home,
"suggestions": suggestions,
@@ -1497,7 +1507,7 @@ def create_app(
prev = locations.get(body.key) or {}
# a lane flip or ack toggle omits the note — leave it alone
note = prev.get("note", "") if body.note is None else body.note.strip()
if opening_id or note:
if opening_id or note or prev.get("away"):
record = {
"opening_id": opening_id or None,
"note": note,
@@ -1510,6 +1520,9 @@ def create_app(
opening_id or None
):
record["acknowledged"] = True
# being out on loan doesn't change where home is
if prev.get("away"):
record["away"] = prev["away"]
locations[body.key] = record
else:
locations.pop(body.key, None)
@@ -1541,6 +1554,32 @@ def create_app(
save_measurements(cfg, measurements)
return _shelf_state()
@app.post("/api/away")
def api_away(body: AwayBody) -> dict:
"""Where the box IS right now, when that isn't home — the travel
case, a friend's house. Home (the opening) stays reserved; ""
brings it back."""
with lock:
revision["n"] += 1
freshen()
games = read_games()
if body.key not in games:
raise HTTPException(404, "no such game in the library")
locations = load_locations(cfg)
record = locations.get(body.key) or {"opening_id": None, "note": ""}
where = body.where.strip()
if where:
record["away"] = where
locations[body.key] = record
else:
record.pop("away", None)
if not (record.get("opening_id") or record.get("note")):
locations.pop(body.key, None)
else:
locations[body.key] = record
save_locations(cfg, locations)
return _shelf_state()
@app.post("/api/acknowledge")
def api_acknowledge(body: AcknowledgeBody) -> dict:
"""Accept (or retract accepting) a doesn't-fit placement — the
+58
View File
@@ -1194,3 +1194,61 @@ def test_measurements_overlay(tmp_path):
state = web.post("/api/measure", json={"key": "1"}).json()
g = state["units"][0]["openings"][0]["resident_games"][0]
assert g["measured"] is False
def test_away_whereabouts(tmp_path):
"""Where it lives vs where it IS: an away note marks the box out
(travel case, a friend's house) without touching its home opening,
contents ride along, and "" brings it back cleaning up records
that held nothing else."""
web, cfg = _web(
tmp_path,
{
"1": _entry("Frosthaven", 12.0, 12.0, 7.0, bgg_id="174430"),
"2": _entry(
"Frosthaven: Solo Scenarios", 8.0, 8.0, 1.0, stored_in="174430"
),
},
)
save_furniture(
cfg,
[
{
"name": "Wall",
"openings": [
{
"id": "big",
"label": "A1",
"zone": "",
"width_in": 26.5,
"height_in": 13.25,
"depth_in": 15.4,
}
],
}
],
)
save_locations(cfg, {"1": {"opening_id": "big", "note": ""}})
state = web.post("/api/away", json={"key": "1", "where": "travel case"}).json()
a1 = state["units"][0]["openings"][0]
frost = next(g for g in a1["resident_games"] if g["name"] == "Frosthaven")
solo = next(g for g in a1["resident_games"] if "Solo" in g["name"])
# home unchanged — still a resident, still packed — but marked out,
# and the contents ride along on the trip
assert frost["away"] == "travel case"
assert solo["away"] == "travel case" and solo["inherited"]
assert frost["lane"] # the home spot stays reserved
record = load_locations(cfg)["1"]
assert record["opening_id"] == "big" and record["away"] == "travel case"
# coming home clears the field but never the home
state = web.post("/api/away", json={"key": "1", "where": ""}).json()
assert "away" not in load_locations(cfg)["1"]
# an unshelved game can be out too; bringing it back leaves no husk
web.post("/api/away", json={"key": "2", "where": "loaned to Ben"})
assert load_locations(cfg)["2"] == {
"opening_id": None,
"note": "",
"away": "loaned to Ben",
}
web.post("/api/away", json={"key": "2", "where": ""})
assert "2" not in load_locations(cfg)