Commit Graph
160 Commits
Author SHA1 Message Date
Eric WagonerandClaude Fable 5 883de3606f v1.1.0: the library learns where everything lives
Version bump for the post-announcement era: the static export (with
the colophon and the Powered-by-BGG badge slot), box dimensions and
the dims report, boxes-within-boxes containment with pick-and-tick
bulk adds, and the whole Shelves layer — furniture drawn as it really
stands, two-lane packing, the human-overrule vocabulary (acknowledged,
wedged, measured, away), and shelves shipping with the export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
v1.1.0
2026-08-09 16:35:42 -04:00
Eric WagonerandClaude Fable 5 7ed21de82a The space view lists boxes, not their contents
Eric, looking at the Red Dragon Inn shelf: a game stored inside
another box doesn't need its own row in the space view — it's clutter.
Right: the shelf holds physical boxes; what's inside them is the
container's business, already listed on its game page.

The opening sheet now renders only physical residents, collapsing
contents into a "holds N inside" hint on the container's row (hover
for the names). Eric's real B2 goes from 20 rows to 5 — Speechless,
Superfight, the Character Trove ("holds 15 inside"), Smorgasbox, Throw
Throw Avocado. The exported wall page mirrors it with "(+N inside)"
after the container's link. The payload keeps every rider (with a new
via field naming its box) so the sheet's add-a-game search still
excludes games already present through containment, and riders still
never appear in the unshelved backlog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 16:21:58 -04:00
Eric WagonerandClaude Fable 5 0ed2145f80 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
2026-08-09 16:17:31 -04:00
Eric WagonerandClaude Fable 5 6c95953dc0 "It's here anyway" + the tape measure: the human overrules the packer
Eric, looking at a dense cube full of "no room" and "unmeasured":
how do I say it really IS here, and in what orientation? Two answers.

The packer models one flat pile and one standing row; real shelves are
3-D — boxes sit crosswise, ride on top of piles, stand proud of the
edge. Acknowledge generalizes from overhang-only to the human
overruling the model: "it's here anyway" on any resident the lanes
can't take. An acknowledged box takes a lane when one works (honoring
a flat/standing override; the spine-out case lands relaxed standing,
now tagged ", proud" by the packer itself instead of guessed from
misfit-ness) and otherwise is WEDGED IN: really there, in some
arrangement the model can't see, outside the capacity bars and never
warned about. The previous commit's ackable gate is gone — accepting
always does something now, so no placebo remains to guard against.

And "unmeasured" is no longer a dead end: the chip is a button. Tap,
enter width × length × depth from your own tape measure, and it saves
to data/measurements.json (a new committed curation store) with dims
source "measured" — overlaid on every games.json read (web, dims,
export, CSV import), because the owner's ruler outranks BGG's
database. All three fields empty clears one.

Verified live: Etherfields on the tower reads "standing, proud", an
acknowledged box in a width-starved cube reads "wedged in", the
measure form opens with three inputs, zero page errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 16:10:35 -04:00
Eric WagonerandClaude Fable 5 4952e69920 The one-way lane flip: an override into no-room now carries its undo
Eric tapped "flat" on Flick 'em Up in a packed cube. The flip means
"stand this instead" — but the cube's standing lane is 1.55 inches and
the box is 2.8 thick, so the override landed it laneless. And laneless
meant no lane button: the override that caused the problem had no
control left to revoke it. A one-way door, sprung mid-curation.

Now an override that leaves its box without a lane renders as
"no room ✱ undo" — one tap clears the override and the packer chooses
again. The plain "no room" chip (no override, nothing to undo) stays a
chip. Verified live on the real B1: flip-trapped box showed the undo,
one tap returned it to the flat pile, override cleared from
locations.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 15:58:39 -04:00
Eric WagonerandClaude Fable 5 f0cf4ec271 No placebo buttons: accept-overhang only offered when it would help
Eric clicked accept-overhang on two boxes in a Kallax cube and got an
"overhang ✓" pill with no lane behind it. The cube's flat pile claims
11.7 of 13.25 inches of width, leaving 1.55 inches of standing lane —
acknowledging waives depth and height, never width, so nothing changed.
That opening is overfull, not overhung, and the button was a placebo.

The sheet now probes before offering: a laneless resident gets the
accept button only when acknowledging would actually land it a lane;
otherwise it's marked "no room" with the honest tooltip. Acknowledged
boxes that DID land show "▮ standing, proud" (their lane isn't
flippable — proud of the shelf is the whole point), plus the
retractable "overhang ✓". Verified against the live wall: the three
Etherfields monoliths on the tower's big row read standing-proud, the
squeezed cube residents read no-room, zero page errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 15:53:02 -04:00
Eric WagonerandClaude Fable 5 7d7d3bb8da The acknowledged overhang: "doesn't fit, shelved anyway" goes legit
Eric's office towers are 9.5 inches deep; Etherfields boxes are 11.8
inches square. Spine-out they stand 2.3 inches proud of the shelf edge
— clearly livable, it's how they sit today — but the lane model will
honestly refuse forever: can't stand (11.8 > 9.5 depth), can't stack
flat (same), so the opening warns NO PACKING FITS. The first deliberate
misfit deserves first-class status.

An assignment can now carry acknowledged: true. An acknowledged box
takes the standing lane WITHOUT the height/depth check — the human has
seen the overhang — while the width budget stays honest (nine
acknowledged monoliths still overflow a 23.25-inch row). It drops out
of misfit/laneless warnings everywhere: the wall diagram, the sheet,
and bggpipe dims (which now says "overhangs, acknowledged" instead).

Ways in: an "accept overhang" button in the opening's sheet (appears
on any resident the lanes can't take; tap again to retract), or an
acknowledge column (yes) in shelve --import. Once acknowledged, later
imports needn't re-state it.

Re-import hardening, prompted by Eric's "will a new import make a
mess?": it's an upsert, not a reset — and now genuinely so. Previously
a re-import preserved notes but silently wiped lane overrides; now
notes, lanes, and acknowledgments all survive when the opening is
unchanged (a move resets lane and acknowledgment — new spot, new
verdict). Same fix in /api/locate: the lane-flip button used to wipe
the hand-entered note because LocateBody.note defaulted to "" —
it's now tri-state, None means leave it alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 15:45:36 -04:00
Eric WagonerandClaude Fable 5 4ce7706521 The arrow that unbound half of wire(), and openings for the unit's top
Eric: styled buttons look great, but edit-description does nothing.
One missing character — .forEach(b = instead of (b => — turned the
callback into a strict-mode ReferenceError that killed wire() midway:
everything bound before the typo (rename, duplicate) worked,
everything after (edit description, its save, the add-opening submit)
silently never bound, and the throw hid inside an async refresh where
no pageerror fires. Fixed and verified live: description edits save,
add-opening works again.

And Eric's next question answered in code: things live ON the
furniture too. The add-opening form gains an "at the top" checkbox —
new openings (or whole grids) land above the existing rows instead of
below, so "on top of the Kallax" is one labeled opening away. Give it
real width/depth and the height to your ceiling, or no sizes for a
no-limit display spot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 15:22:56 -04:00
Eric WagonerandClaude Fable 5 2f526335e8 Every button looks like a button: .linkish retires
Eric's screenshot: "rename" and "edit description" rendered as bare
underlined links beside properly-drawn buttons. The .linkish class
deliberately stripped the design system — introduced for one inline
case, it spread to toolbars where it read as missing styling. All six
sites (rename, edit description, the stored-in change control, the
search-BGG entry point) now inherit the app's real button treatment,
and the class is deleted so it can't spread again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 15:16:40 -04:00
Eric WagonerandClaude Fable 5 79d42c43ab The export summary admits the shelves page exists
Eric re-exported to see the new shelves page, got the same summary
line as always, and reasonably concluded nothing happened — while the
wall page sat freshly written on disk. A feature that ships silently
looks like a no-op. The echo now reads "exported N game page(s) + the
shelves page" and the summary dict carries shelves: bool, pinned by
test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 15:13:56 -04:00
Eric WagonerandClaude Fable 5 10f99e23f5 Duplicate a unit: describe the furniture once, stamp out the rest
Eric's ask. duplicate-unit clones the STRUCTURE — openings with their
sizes, zones, and descriptions, under fresh ids and the first free
"<name> 2"-style name — never the game assignments. And because a
copy immediately wants a real name, units gained rename: opening ids
are stable through it so locations and export URLs never notice, and
a rename-only edit no longer risks wiping the description (the body
field learned the None-means-leave-alone convention). Tests pin the
disjoint ids, the empty copy, the numbering past taken names, and
the wipe-nothing rename.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 15:10:47 -04:00
Eric WagonerandClaude Fable 5 e90a53fb6c Two-lane packing, and the shelves ship with the export
Eric's wall answered the orientation question with a screenshot full
of red: the capacity model assumed one flat stack per opening, so his
spine-out cubes read 200% full. His reviewer's prescription, built to
its acceptance cases: each opening packs a FLAT lane (thinnest axes
against interior height, claiming the widest flat box's width) and a
STANDING lane beside it (boxes on edge against the remaining width;
standing boxes must fit height and depth upright). The largest-
footprint class lies flat, smaller boxes stand, any box's lane is
flippable per-assignment (▬/▮ toggle in the opening view, ✱ marks an
override), and ⚠ now means NO packing fits — not merely "tall stack".
Unmeasured boxes take no lane but keep their honesty tag; dual fill
bars show each lane's budget; a broken opening is never offered by
the suggester. On the real wall: the double-wides and half the cubes
went green (7 games = 13.12" flat + 2.91" standing), and the
remaining warnings mark cubes that genuinely hold 12-16 boxes.
Both reviewer acceptance tests pass verbatim.

And the export gains the humanity Eric asked for: game pages say
where each box lives, containers list their contents, and a shelves
page draws the wall as it physically stands — proportional cells,
zones, descriptions, every opening linking its residents. Publishing
a shelf layout is a choice: --no-shelves keeps the layer out.
"shelves" joins "art" as a reserved slug. 387 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:53:42 -04:00
Eric WagonerandClaude Fable 5 d6ff5e73e9 The docs catch up with the shelves the audits built
Eric asked whether the shelf work was documented — the guide and Help
described the launch version, not the three audit rounds and design
passes since. Both now cover the physical wall rendering, the
suggestion doctrine (♥ reunification, remaining-room honesty, tightest
fit, honest empties), descriptions at both furniture levels,
row-clamped reordering, grid continuation on existing units, chained
containment, and the fill-bar epistemics. The tour gains a Shelves
entry with a screenshot of the drawn Kallax, and four documents stop
claiming the app has seven pages — Shelves made it eight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:34:48 -04:00
Eric WagonerandClaude Fable 5 7ac80d1526 The opening description hint speaks at opening scale
Eric's catch: the placeholder still said "tall bookcase by the
window" — a sentence about a UNIT, sitting in the opening's form.
Now "oversized boxes lie flat here — bottom row, easy reach".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:32:49 -04:00
Eric WagonerandClaude Fable 5 e58cc789f3 Units get descriptions too — both levels of the furniture speak
Eric's correction: the opening-level description shipped, but the
unit deserved one as well (my own example sentence was unit-scale
prose). Units take a description at creation and edit it in place —
a linkish add/edit control under the unit's name with an inline
input — shown as quiet meta text on the card and stored in
furniture.json. Label addresses, zone matches, descriptions explain;
now at both scales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:30:58 -04:00
Eric WagonerandClaude Fable 5 ce4b0e09d4 Openings get descriptions: zone is the keyword, this is the sentence
Eric's ask. Free text on any opening ("tall bookcase by the window —
kids reach the bottom rows"), edited in the opening settings sheet,
shown under the sheet's title, and surfaced as the cell's hover title
on the wall diagram. Stored in furniture.json like everything else.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:29:44 -04:00
Eric WagonerandClaude Fable 5 a23b97d2f9 Opening moves become row-honest: ← → within the row, clamped at edges
Eric asked what the ↑↓ buttons did — the honest answer was "reorder a
flat list that no longer exists, and fragment your wall diagram if
pushed across a row boundary" (A1, B1, A2 renders as THREE rows under
the letter-grouped layout). The useful half survives as ← → "move
within its row"; the endpoint clamps at row-letter boundaries so the
diagram can't fragment. Tests pin both the clamp and the legitimate
within-row swap; the acceptance flow's reorder step updated to match
the new semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:28:15 -04:00
Eric WagonerandClaude Fable 5 75162c3e2e The suggestion pills return: the client learns the object shape
Eric: "Everything says no matching openings yet. How would I get
started?" — because the client still mapped suggestion STRINGS while
the server sent {id, reunites} objects. The edit teaching the pills
the new shape (and the ♥ reunification marker) died in a script abort
during the ranking batch, and the retry re-applied everything except
it; the arrangement screenshot that "verified" the batch predated the
ranking change, so no check ever looked at pills until now. Verified
against the real library this time: 296 pills, zero false
no-matching rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:24:25 -04:00
Eric WagonerandClaude Fable 5 f4f3877651 Units draw their real arrangement; suggestions learn the owner's plan
Three requests from Eric plus his reviewer's ranking doctrine.

The unit renders its PHYSICAL shape: openings group into rows by
their label letters and each cell's width is proportional to its
interior width — the Kallax reads as two double-wides spanning the
top, then four-across cube rows. A diagram of the wall, not a list.

The toggle buttons died: "Add a unit" and "Add an opening" are
labeled, always-visible forms — the button that merely revealed the
form below it was ceremony.

Suggestions got their relevance doctrine, three rules deep:
(1) Reunification beats geometry — an opening holding a series-mate
or base game (kinship = the normalized pre-colon name stem, plus the
series field where enrich has one; no taxonomy invented) ranks first,
marked ♥. Every placement the owner confirms teaches the suggester
their organization by example; expansions chase their base games
automatically during the move. (2) "Fits" means the REMAINING
opening: the stack budget already spent is subtracted before
offering, so the suggester's honesty holds precisely as shelves fill
— the direction it used to degrade. (3) Then tightest verified fit,
so a small box is offered cubes and never the oversize row. Nothing
matching says "no matching openings yet" instead of offering
everything the wall has.

One composed test pins the interplay: a full shelf isn't offered even
to a series-mate (honesty outranks reunification), and freeing the
space flips the same suggestion to ♥-first. 382 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:20:16 -04:00
Eric WagonerandClaude Fable 5 446579e96c Asset stamps become content hashes — version stamps bust nothing
Eric's screenshot showed week-old CSS again, straight through the
cache-buster: the stamp was the app VERSION, unchanged at 1.0.0 since
release, so every stylesheet change shipped at the same ?v= URL and
browsers rightly kept their copies. The stamp is now an 8-char hash
over the static bundle's bytes — it changes exactly when the files do,
releases or not. The test now pins that property instead of the
version equality it used to celebrate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:10:54 -04:00
Eric WagonerandClaude Fable 5 708883313f The per-unit add form learns grids — and Eric's unit comes back
Eric walked the real flow: top row of double-wides done, now add
three rows of four cubes — and found no way, because grid creation
lived only in the CREATE form (the "repeat with another grid" advice
pointed at a non-obvious trick). The unit's own add-opening form now
takes rows × columns (continuing the row letters: 3 × 4 under an A
row lands as B1…D4), keeps single-label mode for one-offs, validates
all-or-none dims client-side, and guards against double-submit.

Also restored: Library Kallax. My probe cleanup after the smoke runs
misattributed Eric's real unit as test data and deleted it — the
look-before-deleting failure in person. Rebuilt with the intended
geometry (two 26.5" double-wides, zone oversize) and the twelve
cubes, committed as his data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:07:30 -04:00
Eric WagonerandClaude Fable 5 6ab4a836b4 Audit round 9 lands: the location layer keeps its promises
Five blind reviewers over the day-old shelves layer, ~28 verified
findings — plus Eric's screenshot catching the biggest one live: the
generic .card is flex (built for review's photo-beside-ballot layout),
so unit headers shared a row with their grids and unshelved rows
flowed horizontally off the page. Shelf cards are now .card.stack.

The model fixes. Containment chains resolve recursively with a cycle
guard — minis inside an insert inside a big box live where the big box
does, instead of vanishing from every list; the stored-in endpoint
walks the whole chain when refusing cycles, and clears the newly
contained game's own shelf spot (one box must never consume capacity
in two openings). Contained games are listed residents but occupy no
shelf space: only physical boxes are stacked and fit-checked — the web
report now agrees with the dims report about the same opening. A
location pointing at a vanished opening (hand-edited or reverted
store) SURFACES as unshelved with a "shelf gone" chip in the app and
counts as homeless in the CLI, instead of hiding the game from every
list while the page declares everything has a home.

Honest edges. Opening dimensions are all-or-none everywhere (a
half-sized opening silently became limitless; the CLI report crashed
formatting it); a second grid on a unit continues the row letters so
labels stay unique and label-addressed CSV imports keep working, and
row letters survive past Z; CSV re-imports preserve hand-entered
notes; the ambiguity reject names the fix that actually works;
corrupt furniture/locations stores speak a 500 instead of a raw
traceback; the dims help text stops saying Kallax; DIM_AXES gets one
home in models.py instead of three drifting copies; the new stores
join CLAUDE.md's commit registry.

The page behaves. Custom-dims fields hide unless the custom preset is
chosen (typed values were silently discarded); the sheet is a real
dialog (role, aria-modal, Escape, focus return, one layer at a time);
backdrop close requires press AND release on the backdrop (a text-
selection drag out of the search box no longer dismisses); refresh
goes through changeGate and stops wiping the search mid-interaction;
the prompt() chain is an inline per-unit form with client-side
all-or-none validation; unit-create only toasts success after the
openings actually land, recovers from its own half-failures, and
guards against double-submit (a click retried against the re-rendering
DOM built a second grid — caught live in a Playwright run); warnings
speak (aria-labels on ⚠ and overfull in the fill bar's label);
unmeasured boxes are visible in cells and sheet rows; the library's
unshelved filter matches the Shelves page's definition; reorder
buttons have names; the detail locform wraps at phone width.

Eight new regression tests from the seats' sketches; 380 total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:02:00 -04:00
Eric WagonerandClaude Fable 5 4f446f6f2a The Shelves layer: where every box physically lives
Eric's spec, all nine points. Two committed local-only stores follow
the local_games.json pattern — furniture.json (units of openings with
interior dims; a dimensionless opening is a virtual spot like a travel
case) and locations.json (game key -> opening + note). Shelf layouts
are nobody's data but the owner's; nothing touches upload.

The Shelves page builds furniture without hand-editing JSON — the
acceptance bar (two double-wides above three rows of four cubes, two
bookcases, a travel case) is a TEST, driven entirely through the
endpoints the UI calls. Presets for Kallax/Billy/custom/virtual,
grid creation with A1-style labels, openings editable/deletable/
reorderable. Units render as grids: zone, count, fill bar (stacked
thinnest-axis vs interior height), ⚠ on overfull or any resident that
can't fit. Openings open as a modal — a bottom sheet at phone widths,
search-first with thumb-sized targets for the moving-day loop.
Unshelved games list alongside with one-tap suggestions (only openings
they verifiably fit, with room).

Containment composes: a game stored inside another box inherits its
container's location, rides along in the opening's resident list
(marked), and refuses direct assignment naming its container. The
detail page's where-it-lives card gains the picker (openings grouped
by unit, each labeled fits / doesn't fit / can't verify) plus virtual
notes ("lent to Sarah, June"); the Library list shows a location line,
filters by unit or unshelved, and search matches location text and
zones.

bggpipe dims drops its hardcoded Kallax for the user's actual
furniture: per-opening capacity, overfull and misfit warnings,
unshelved count. bggpipe shelve --import loads a name,opening CSV
(ids or labels), rejecting — never guessing — unknown names, ambiguous
copies, unknown/ambiguous openings, misfits, and contained games.

Ten new tests incl. the acceptance flow, inheritance, CSV rejects,
and a phone-sheet smoke; 372 total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 13:45:36 -04:00
Eric WagonerandClaude Fable 5 e85f72c546 The where-it-lives card actually behaves; assets stop serving stale
Eric, again with the screenshot that tells the truth: "when I click it
it greys out and nothing seems to happen." Two roots. My own
#storedform { display:flex } silently DEFEATED the hidden attribute
(an author display beats the UA's [hidden] rule), so the form was
visible from load and the click's only visible effect was disabling
the link. The rule is now :not([hidden])-scoped, the click swaps the
sentence for the form and focuses the select. And the "unstyled"
select he saw was last week's stylesheet: /static assets had no
cache-busting, so my fresh-fetch Chrome verification passed while his
browser held the old CSS. Asset URLs now carry ?v=<app version> —
an upgrade busts every browser's cache by construction.

Verified the full interaction in-browser this time: form hidden on
load, click reveals with 142 container options in a styled select.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 13:14:55 -04:00
Eric WagonerandClaude Fable 5 f435cffea0 Selects join the design system at the element; entities decode at the parser
Eric, correctly: "Didn't learn from last time I see :)". The container
select in the where-it-lives card was native — because the previous fix
styled .editform select, the container, not the element. The rule is
now bare `select` (every page here is ours), so the fourth dropdown
gets it for free, and the CSS comment records the lesson.

Same screenshot, same class of miss: the About card showed raw &rsquo;
entities — the export got the double-encoding fix, the app never did.
Fixed at the PARSER this time (models.parse_things_full decodes once,
so every consumer — app, export, games.json — receives plain text),
with a render-time decode covering entries enriched before the fix.
Verified in-browser: "Great Britain's history", apostrophe and all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 13:07:52 -04:00
Eric WagonerandClaude Fable 5 fb611e0c87 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
2026-08-09 12:55:36 -04:00
Eric WagonerandClaude Fable 5 f825a91ea4 Editions become changeable — losslessly
Eric hit a diff version-disagreement and found there was no way to
revisit a set edition: the Titles pick-edition button only appeared
while the version was UNSET. It now shows as "change edition" on
version_auto/version_approved rows — and reopening the ballot no
longer clears the current pick, because change must be lossless:
abandoning the ballot keeps the approved edition, the lingering id is
inert while ambiguous (is_confident_version gates diff and upload),
and the next decision overwrites or clears it.

Help documents the other half of the disagreement story: the pipeline
NEVER edits a version already set on a BGG collection entry (the
additive-only rule), so a diff disagreement is resolved either by
changing the pipeline's pick here or by fixing the entry by hand on
BGG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 12:28:59 -04:00
Eric WagonerandClaude Fable 5 fb9f654cd8 add-checked feedback returns: parse the Response before reading counts
Eric's report: no toast, no list update — yet the add landed. apiPost
returns the raw Response (every other caller only truth-checks it);
reading .added off it gave undefined and .skipped.length then THREW,
killing the toast, the panel close, and the refresh in an unhandled
rejection. The handler now parses the body first. Verified live: toast
fires with the count, panel closes, the catalog updates in place
(probe row removed from the data afterward).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 12:20:32 -04:00
Eric WagonerandClaude Fable 5 06902538fe The pick panel earns its styling
Eric's screenshot told the story: .editform label's field styling
(stacked, uppercase, gray) was swallowing the checklist rows, and the
container select was browser-native. Pick rows override back to plain
reading lines — checkbox, name in ink, year·type as quiet meta, the
already-cataloged chip pushed to the row's edge, hover tint — inside
a bordered scrollable pane; selects join the design system alongside
inputs. Verified against a live 117-result search.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 12:11:42 -04:00
Eric WagonerandClaude Fable 5 e153556fbf Help documents the pick-and-tick flow
Follow-up to dbc1899: the in-app Help's curation section now covers
the search-BGG checklist, containment, and the decides-instead-of-
duplicates behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 12:06:23 -04:00
Eric WagonerandClaude Fable 5 dbc1899759 A box of expansions in one pass: search, tick, done — with containment
Eric's Character Trove problem, both halves. The Titles add panel gains
"search BGG and tick them off": one API search (rate-limited, cached)
returns the whole family as a checklist, already-cataloged ids greyed
out, and every ticked result lands as an APPROVED match row plus a
title addition — the human picked it off BGG's own list, so resolve
has nothing left to derive. A pick whose name matches an undecided
photo line decides THAT line (photos kept) instead of duplicating it;
BGG's true name twins (two games both called "Citadels") skip with an
honest message rather than fusing.

And the half Eric spotted mid-build: containment is real data, not a
convention. A stored_in column on the match row (the container's
bgg_id — human curation, riding the same durable CSV as dedupe_veto)
is set by the pick panel's "they all live inside" selector, flows
through enrich onto games.json, and surfaces both directions in the
Library — "where it lives" on the content, "in this box" on the
container. The dims report excludes contained games from the Kallax
unknowns and counts them separately: a game with no box of its own
has no shelf space to plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 12:05:43 -04:00
Eric WagonerandClaude Fable 5 102507b040 Box dimensions: enrich learns shelf math, dims reports the Kallax truth
Eric's brief, implemented to the letter. BGG keeps physical dimensions
on VERSIONS, not games, so enrich runs a second cached pass over
thing?versions=1 (same batching, token, rate limit, and cache as every
call). A game with a chosen version takes that exact version's numbers
(source "version", mirrored onto its version dict); a versionless game
gets numbers only when every printing with data agrees within 0.5" per
axis (source "unanimous", keeping the MAX per axis — the planning
question is "will it fit"); disagreement stores nulls as "conflicting"
— never a guess — and BGG's 0 parses as "never entered", not a real
dimension. rpgitems and local games are "absent". Read-only: upload
untouched.

The new offline `bggpipe dims` reports coverage by source, the ten
biggest footprints, and a Kallax fit check (13.2" square opening,
15.4" deep; a box fits if SOME orientation puts two axes through the
opening within the depth) — naming every misfit and every game whose
dimensions can't be verified, because can't-verify ≠ fits. Trusted
numbers surface on the Library detail page as a "box" row.

First real run: 54 version-exact, 16 unanimous, 39 conflicting, 27
absent; three genuine misfits (Bugs in the Kitchen's 17" box, History
of the World and Risk LotR both over the 15.4" depth).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 11:18:48 -04:00
Eric Wagoner 9d752c4109 README: link the live game library built from the enrich stage 2026-08-06 19:23:06 -04:00
Eric WagonerandClaude Fable 5 47409d0487 The colophon card: the shelf ends with the piper and the fine print
Eric's idea — the last card in the grid, always visible, carrying the
full piper, the Powered-by-BGG badge, and the legal text. A <div>
rather than an <a>, so the search filter (which selects a.game) can
never hide it: the compliance text is part of the collection, not a
skippable footer. The piper ships from the package's own static art,
credited to Juniper on the card itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 19:07:12 -04:00
Eric WagonerandClaude Fable 5 c7fdc60f87 Audit round 8: the export earns its publishing promises
Five blind reviewers over the day-old export stage; ~30 findings
verified, the big ones sharing one root — a static-site generator
makes promises a pipeline stage doesn't, and the first cut kept none
of them.

URL stability was empirically false two ways: adding an edition whose
key sorted first STOLE the base slug (every colliding URL reshuffled),
and removing the base holder renumbered survivors over the stale
pages' corpses — wrong content at live URLs, not even 404s. And
nothing ever deleted anything: removed games stayed published forever.
One mechanism fixes all of it — a manifest (.bggpipe-export.json) in
the output directory records which slugs the export owns and which
source URL produced each cover. Slugs persist across runs (a published
URL never moves and can never be stolen), stale pages are removed
(only ever manifest-claimed ones — user files are not ours to touch),
replaced box art re-fetches when its URL changes, and "art" is a
reserved name so a game called Art can't move into the asset dir.

Trust-the-network fixes: a 200 response must LOOK like an image (magic
bytes + size) before it's cached, else a CDN interstitial became a
permanent "cover" that re-runs skipped forever; downloads go through
fsio.atomic_write_bytes instead of a hand-rolled fixed-tmp-name dance
(the exact hazard fsio's own docstring warns about); a missing
hand-added cover counts as a failure instead of silently shipping
coverless; the badge file is sniffed too; CDN pacing raised to 1s and
written into the spec as an adjudicated carve-out rather than a code
comment's private opinion.

Ship-shape: pages write atomically with the index LAST (a killed run
can't publish links to pages that don't exist); the CLI exits nonzero
on failures so `export && rsync` can't publish an incomplete site;
footer/fine-print contrast now clears WCAG AA on the sky background;
meta description, og:title/og:image and a favicon stop bare unfurls;
the BGG link moved out of the h1; the noart tile is aria-hidden; the
search box gained a no-matches message; numeric fields from enrich
render instead of crashing the join; years and ids are escaped; the
players/playtime formatters are aligned with their JS twins and both
sides carry keep-in-sync constraint comments; export moved after
enrich in the CLI listing.

Twelve export tests now, including the previously-vacuous atomicity
test rebuilt to actually interrupt a write. One honest loose end: one
full cover re-fetch occurred during rollout that the identical naming
code can't explain; the manifest's URL records make any recurrence
diagnosable. 352 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 18:51:05 -04:00
Eric WagonerandClaude Fable 5 f291b9c190 The Powered-by-BGG badge stays out of the repo
It's BGG's asset, provided through the owner's registered application
page — redistributing it in a public repo isn't ours to do. Each user
saves their own as data/powered-by-bgg.png (the export says so when
the file is absent), same principle as API tokens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 18:34:55 -04:00
Eric WagonerandClaude Fable 5 f677c7ce65 bggpipe export: the library as static pages for any site
Eric wanted the library on his blog; the spec always called games.json
"the seed for a future web frontend." The new export stage renders it
as self-contained static pages — an index with search, one page per
game with facts, chips, the owner's edition and the description — that
drop into any static host (Hugo's static/ folder included). No server,
no build step, no external requests from the published pages.

Public pages carry obligations a localhost app doesn't. Cover art is
downloaded once from BGG's CDN instead of hotlinked (0.3s between
fetches — a guest, not a crawler; part-file writes so a failure never
leaves a truncated image; re-runs skip what exists, so the export is
idempotent and resumable like every stage). The footer shows a
Powered-by-BGG badge per BGG's public-app policy — text by default,
upgraded to the official logo when the owner saves it from their
registered-application page as data/powered-by-bgg.png — plus the
trademark attribution. And one privacy rule, tested: shelf photos are
never exported; they picture the inside of the owner's home. Covers
and hand-added local art only, per Eric's explicit choice.

Slugs are deterministic and collision-stable (two editions of one game
get -2 suffixes in sorted-key order) so re-exports keep every URL.
Descriptions un-double-encode BGG's entities. First real run: 136
pages, 254 covers, 64MB, live on the blog's static directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 18:34:23 -04:00
Eric Wagoner e778452dd1 README: add live visitor and install badges 2026-08-06 18:11:50 -04:00
Eric WagonerandClaude Fable 5 1f1d50e649 Contact grows two social channels: Mastodon and Bluesky
Eric's call — the README's questions-and-bugs section and the package
urls now list @eric@toots.kestrelsnest.social and the
kestrelsnest.social Bluesky handle alongside the email.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
v1.0.0
2026-08-06 13:29:17 -04:00
Eric WagonerandClaude Fable 5 0b5c1c727f Pre-announcement sweep: a contact channel, a fresh Titles shot
Three of four reviewer nits. The real gap: the Gitea instance takes no
public registrations, so announcement readers had no way to report
anything — the README now offers the author's email and says plainly
why there's no issue tracker yet. The Titles screenshot still showed
the day-three catalog ("123 extracted titles") — replaced with the
clean run's capture (150 reads, shaky-read badges, the add-a-game
panel) and honest alt text. And the Beta classifier gets a comment
recording that it's deliberate alongside the README's "battle-tested":
tested for one person on one platform is exactly what Beta means.

The fourth item (release notes on the v1.0.0 tag) is a Gitea web-UI
task — notes drafted separately for pasting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 13:25:39 -04:00
Eric WagonerandClaude Fable 5 4e09f11052 The gallery learns from the clean run, and the docs link the proof
Eric's clean-room run produced screenshots of states the gallery
never had: Review showing LIVE ballots (Agricola vs Revised Edition,
with per-candidate ranks, owner counts and view-on-BGG links, and the
search BGG/RPGGeek buttons), the editions pass in full flight (five
English Catan printings to choose from — the version-selector shot
Eric asked for days ago), the Queue with real pending adds and a
pending version update, a dry run's would-add output (now
illustrating the guide's Uploading-safely section), and extract
streaming per-photo counts (now illustrating the guide's web-app
section). The old Review and Queue images showed stub-era done/empty
states with captions about waiting on tokens — retired.

Also linked, at Eric's suggestion: the pipeline's actual output is
public at boardgamegeek.com/collection/user/ewagoner — the README
status line and tour intro now point at it, because "here's the
collection it built" beats any amount of claimed battle-testing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 12:57:27 -04:00
Eric WagonerandClaude Fable 5 e7f04f9e6d 1.0.0 — the version the pipeline earned
Battle-tested against a live collection, clean-room verified, docs
split for humans, audited seven rounds. Eric's call: "1.0.0 for sure!"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 12:54:40 -04:00
Eric WagonerandClaude Fable 5 c3549ddae2 Package metadata fills in: author, urls, keywords, classifiers
Ahead of sharing: authors (Eric, eric@ericwagoner.com), readme,
Homepage/Repository/Documentation urls, keywords, and trove
classifiers (beta, end-user, board games; macOS/Linux listed —
Windows stays unlisted until someone actually tests it). Version was
already handled: dynamic from __init__.py, currently 0.1.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 12:53:19 -04:00
Eric WagonerandClaude Fable 5 22c663d620 The spec moves to docs/ and says what it is
Floating in the root under its planning-era name, the spec read like
leftover scaffolding. It isn't — it's the maintained design contract
(what must stay true, and why) that CLAUDE.md designates as
read-before-changing-semantics. Now docs/spec.md, with a header
saying exactly that and pointing usage questions at the guide; both
pointers updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 12:50:41 -04:00
Eric WagonerandClaude Fable 5 abb64aa40c Docs answer "what will this cost me" with a real number
Eric's clean run put a price on the pipeline's only paid step:
65 shelf photos, 136 games, under a dollar on the default model. The
README's vision requirement and the guide's config section both now
say so, plus the shape of the cost — one-time, since per-photo reads
are cached and re-runs are free — and that the BGG side costs nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 10:28:27 -04:00
Eric WagonerandClaude Fable 5 251fc8fde8 The review done-card stops assuming why titles are unresolved
Clean-run finding #2 from Eric: with a token loaded and resolve simply
not yet run, the card still said unmatched titles were "waiting on the
API token" — stub-era copy hard-coding the only reason unresolved
titles existed back then. The summary payload now carries
token_present (a boolean, never the value — pinned by test), and the
card gives the advice that fits: token loaded → "run resolve";
no token → the waiting message, now with a link to BGG's application
page since that reader hasn't registered yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 10:21:33 -04:00
Eric WagonerandClaude Fable 5 3566a62f2d "remove — not a game" admits its second job
Eric's observation: the button also removes duplicate reads — the
same physical box read differently from two photos, leaving two lines
for one copy — and its label denied that use existed. Now "remove
this line", with the hover title and the confirm dialog naming both
legitimate reasons and pointing the opposite case (one line, several
real boxes) at split. Help's remove entry documents the duplicate-
read use and which line to remove (the worse read; the survivor
keeps its own photos).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 10:11:23 -04:00
Eric WagonerandClaude Fable 5 1d9baff989 Requirements answer the question every BGG tool gets asked
Eric has seen the criticism land on other BGG apps: why does this
thing want my password? The README now answers it where the
requirement appears: BGG has no write API, so uploading means signing
into the real website in a visible browser on the user's own machine
— that login is the password's entire job. And the reassurance that
matters: no server, no telemetry, no analytics, nothing collected;
credentials go to boardgamegeek.com and nowhere else, the only other
contact is the user's own chosen vision provider (photos only, and a
local Ollama keeps even those home). The guide's credentials section
links back and notes the saved browser session stays local too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 10:06:43 -04:00
Eric WagonerandClaude Fable 5 6041150c8d The README stops being four documents wearing one trench coat
Eric's read on the first-visitor experience: 190 lines of pitch,
manual, gallery, and contributor doc is intimidating when the visitor
only needs the first 40. Split three ways:

README.md is now the front door — what it is, why it exists (told in
first person now, since it IS a personal itch scratched), how the six
stages work, requirements, quick start, one hero screenshot, and the
development/citizenship/license notes. Sixty percent shorter.

docs/tour.md carries the full gallery: all seven pages, the game
detail view, and the phone set, captions intact.

docs/guide.md is the complete user's guide: credentials and config,
the stages and their flags, phone pairing, photo technique, curation
stores, RPG handling, upload safety (including the collection-export
lag), the no-token-yet path, and the keep-data-out-of-git rationale.

Every relative link and README→guide anchor machine-verified to
resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 10:03:48 -04:00
Eric WagonerandClaude Fable 5 1df784e253 The CLI loads .env itself — init's promise finally holds
First finding of Eric's clean-room run, and the exact kind the
rehearsal exists for: init writes credentials to .env, but nothing
ever loaded it — the dev repo's committed .envrc + direnv did it
invisibly, and a fresh directory has neither. The web banner then
advised "run bggpipe init or load .env", circular counsel for someone
who just ran init.

A typer callback now loads ./.env before every command, using the
same parsing rules as the wizard that writes it (export prefixes,
quoted values, quoted-empty = unset). Real environment variables
always outrank the file, so direnv setups and explicit overrides keep
working unchanged. Verified in a scrubbed-environment clean room: the
credentials banner is gone with nothing but .env present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 09:50:49 -04:00