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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 ’
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
The one outbound link in the whole app was the detail-page crumb, and
it sent RPG items to a /boardgame/ URL — the wrong home for things
that live on RPGGeek. A shared bggUrl(id, type) helper now routes by
type, and the link grew into the places where opening the real page is
the point: every matched name on the Titles catalog (verifying a match
IS opening its page), each Review ballot candidate (a "view ↗" that
stops propagation so checking a candidate doesn't vote for it), and
the version ballot's title links to BGG's own /versions list.
Verified against live data: RPGs route to rpggeek.com, board games to
boardgamegeek.com, 127 outbound links on the Titles page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
Eric caught two leftovers. The Quick start still opened with git clone
— the exact path the last commit built two warning layers against —
and now leads with uv tool install into a directory of your own, with
the stage commands dropping their uv run prefix to match (the clone
recipe moves to Development, its one remaining legitimate home, with
a pointer to run real pipelines elsewhere).
And the requirements line claimed macOS or Linux. Nothing in the code
is platform-specific — pure Python, pathlib throughout, and uv,
FastAPI, and Playwright all support Windows — so the honest statement
is: Windows works but is untested, with one real caveat named in
place: the owner-only chmod on .env and browser session state is
POSIX-only, so Windows users keep those files in an account-protected
directory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
Eric asked whether AI residue had crept in since the last credibility
pass. It had, in two forms — both descendants of the audit's
"row-edit flow is collid-exact now" fix, which corrected upload.py but
not everyone who believed the old constraint:
The deferral scaffolding survived its own removal: build_queue still
returned a permanently-empty deferred list, run_upload still unpacked
it and carried a dead echo branch repeating the DISPROVEN claim ("the
row-edit flow can't target a collid"). And diff.py held a vestigial
guard on the same false premise, refusing to upgrade a versionless
copy whenever a sibling carried a version and telling the human to do
it by hand — the collid-exact editor makes that upgrade safe, so the
guard cost a real capability and its test now pins the opposite.
A comment audit (13 findings, verified line-by-line) cleaned the rest:
the webreview docstring counted six pages of seven and claimed
"localhost only" (--lan exists); the nav comment still said "catalog"
for the page renamed Titles; extract's docstring credited "Claude
vision" though any OpenAI-compatible endpoint dispatches; and five
comments narrated retired bugs ("the original bug", "why the old
guess quit after page one", "made the count go negative") — each
rewritten as the present-tense constraint or trimmed, per the
standing rule: comments state what the code can't show, never how it
got that way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
Eric spotted it: the README told people to clone this repo, rm the
committed data, and run — which writes THEIR pipeline artifacts at
git-TRACKED paths. The next `git pull` (this repo commits data every
session) refuses to merge, and the internet's standard remedies for
that error — reset --hard, checkout ., stash, clean -fdx — destroy
their review decisions, hand-written games, upload log, and photos.
Two layers. The README's "Bring your own shelves" now leads with
`uv tool install git+…` and running in a directory of your own: data
lands untracked by construction and a bug fix is `uv tool upgrade`,
which cannot touch it. And because nobody re-reads a README, Config
gains tracked_data_warning(): if artifacts under data_dir are
git-tracked, `bggpipe init` and the web dashboard both warn in plain
words. The owner's exemption is data/.own_repo — a GITIGNORED marker,
so the author's checkout is silent while a fresh clone of the same
repo still gets the warning (a committed marker or config key would
have shipped the exemption to exactly the people who need warning).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
Eric flagged the detail pages as worth showing. Britannia's page joins
BGG's stats with the pipeline's own knowledge — the owned edition and
the shelf photo provenance — which is the pitch in one image.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
The web layer's serialization story had three gaps: /api/run started a
stage without the lock, so a decision mid-save could pass the rewrite
guard and still be clobbered by the stage's full rewrite (now the start
itself serializes); /api/photos accepted a replacement photo while
extract was running, permanently pairing the new bytes with the old
photo's reads (now refuses like every other mutation); and /api/queue
read session rows lock-free and stale (now freshens under the lock).
The localhost Host allowlist applied only to writes — a DNS-rebound
page could read pipeline state and shelf photos with plain GETs; it
now covers all methods (foreign-Origin reads still pass: without CORS
headers a cross-origin page can't read the response anyway).
Data-loss finds: the off-BGG edit form re-rendered from games.json,
which only sees hand data after enrich — so a second save resubmitted
pre-save blanks and cleared the first (the detail endpoint now overlays
local_games.json live). The local key embeds the photo list, so a new
sighting orphaned hand-written facts silently; enrich now migrates them
when the title still matches exactly one line, and warns instead of
ever dropping. research() left the previous game's version verdicts on
the row, riding a stale version_id onto the next pick; it clears all
four fields as reopen does. find_row now prefers the version-open
sibling on duplicate keys, mirroring _adopt. Re-adding a removed
hand-added title silently no-opped behind a 200 — it now rescinds the
removal (an explicit undo), and a true duplicate add answers 409.
Smaller: parse_search's dedupe collapsed same-id rows under DIFFERENT
names, discarding the alternate-name row whose exact match downstream
scoring needed (now collapses same-name only; research merges its
ballot per game preferring exact evidence); rpgitems rank in their own
family so their rank parsed null; the pipeline badge counted
review-retired queue rows as pending; the catalog pairing cascade ran
per-entry so a tier-3 claim could steal a sibling's exact row (now
tier-by-tier across all entries, as resolve does); library cards
render a lone player bound without "undefined" and the seats filter
tolerates it; added_no_version reads "done · no version" instead of a
bare green done.
Every finding verified against the code before fixing; each fix
carries a regression test. 337 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g