Containment becomes editable: the where-it-lives card grows controls

Eric's question exposed the gap: stored_in was settable only at
pick-time, with no path for a game already in the system. The Library
detail page's "Where it lives" card now renders for every entry — "in
its own box on a shelf" with an it-lives-inside-another-box control,
or the current container with a change button — saving through a new
/api/stored-in endpoint that updates the match rows (the durable
record) AND the library entry in place, so the page and the shelf math
reflect it immediately, no enrich run needed. Guards refuse
self-containment and direct cycles; local games are addressable
through their key's normalized title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqdW79g
This commit is contained in:
Eric Wagoner
2026-08-09 12:55:36 -04:00
co-authored by Claude Fable 5
parent f825a91ea4
commit fb611e0c87
5 changed files with 1484 additions and 137 deletions
+25
View File
@@ -1540,3 +1540,28 @@ def test_library_detail_shows_containment_both_ways(tmp_path):
assert inside["stored_in_game"] == {"key": "173634", "name": "The Trove"}
box = web.get("/api/library/173634").json()
assert box["contains"] == [{"key": "999", "name": "Witchdoctor"}]
# containment is editable after the fact: clear it, then set it back
assert web.post("/api/stored-in", json={"key": "999", "container": ""}).json() == {
"stored_in": None
}
rows2 = {r["bgg_id"]: r for r in read_matches(cfg.matches_path)}
assert rows2["999"]["stored_in"] == ""
assert "stored_in_game" not in web.get("/api/library/999").json()
assert web.post(
"/api/stored-in", json={"key": "999", "container": "173634"}
).json() == {"stored_in": "173634"}
assert read_matches(cfg.matches_path)[-1]["stored_in"] == "173634"
# guards: self-containment and direct cycles refuse
assert (
web.post("/api/stored-in", json={"key": "999", "container": "999"}).status_code
== 400
)
assert (
web.post(
"/api/stored-in", json={"key": "173634", "container": "999"}
).status_code
== 400
)