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
7.9 KiB
7.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project
bggpipe — a resumable, idempotent CLI pipeline that turns shelf photos into a BoardGameGeek collection, in six stages:
| # | Command | What it does | Status |
|---|---|---|---|
| 1 | bggpipe extract |
Claude vision reads titles + edition cues from photos/ |
working |
| 2 | bggpipe resolve |
match titles to BGG IDs and versions via XML API2 | working (real API data since 2026-08-05) |
| 3 | bggpipe review |
human review of ambiguous/unmatched items; --web serves a FastAPI UI on port 8377 |
working |
| 4 | bggpipe diff |
diff approved matches against the existing BGG collection | working |
| 5 | bggpipe upload |
add games via a logged-in Playwright session | working — all browser flows verified live 2026-08-06 (62 adds + 36 version updates landed) |
| 6 | bggpipe enrich |
fetch full game/version metadata into games.json |
working |
| + | bggpipe export |
render the library as self-contained static pages (covers downloaded, never hotlinked; shelf photos never included; Powered-by-BGG badge slot) | working |
Full design lives in docs/spec.md (read it before changing pipeline semantics); the upload-stage walkthrough is in docs/bgg-upload-flow.md.
Commands
uv sync— install deps (Python 3.12+, managed by uv; useuv add, never pip).uv run bggpipe inithandles first-run setup (folders, .env credentials, the one-timeplaywright install chromium).uv run bggpipe web— the app: seven pages (Pipeline/, Photos, Titles, Review, Queue, Library, Help) in a shared sidebar shell (responsive: hamburger nav + stacked tables under 900px); stage runs execute one-at-a-time in a background job.--lanbinds 0.0.0.0 behind a per-device access key: persisted indata/.lan_key(gitignored), printed as a QR at startup, cookie-paired for a year, required on EVERY network request (loopback clients and/static/*are exempt; the Host/Origin guard still applies). Phone camera uploads (genericimage.jpgnames) get mintedshelf-<timestamp>names — only explicitly-named files trigger the replace-to-reshoot flow.uv run bggpipe <stage>— run a pipeline stage. Non-secret settings come fromconfig.toml(dirs, rate limit,vision_provider+ per-provider[vision.*]blocks — "anthropic" or any OpenAI-compatible endpoint incl. local Ollama);--configoverrides the path.uv run pytest— the suite runs fully offline against fixtures. Tests markedlivehit the real BGG API (read-only) and are skipped unless you pass--run-live.uv run ruff check/uv run ruff format— lint (rules E, F, I, UP, B, SIM) and format.
Layout
src/bggpipe/—cli.py(typer app), one module per stage (extract,resolve,review+webreview,diff,upload,enrich);templates/shell.html+templates/pages/*+static/app.{css,js}are the web UI (the stylesheet is the design system — tokens derive from the mascot art), plusbgg_client.py(rate-limited XML API2 client that caches responses todata/bgg_cache/),jobs.py(single-slot background stage runner for the web UI),normalize.py(title normalization),models.py(dataclasses),config.py,fsio.py(atomic writes),init_wizard.py(first-run setup).scripts/—write_stub_fixtures.py/write_photo_fixtures.pygenerate synthetic fixtures;record_fixtures.pyre-records real API responses once a token exists.tests/fixtures/bgg_cache/— recorded real XML fixtures the offline tests run against.data/— pipeline state (CSV/JSON artifacts are committed; caches are not — see Git).
Hard rules (from spec — never violate)
- ≤1 request every 2 seconds to any BGG endpoint; jittered backoff on 429/503. Upload stage: 2–4 s randomized delay between games.
- Credentials never touch disk or logs.
ANTHROPIC_API_KEY,BGG_USERNAME,BGG_PASSWORD,BGG_API_TOKENcome from env vars only. Playwright storage state is credential-adjacent — keep it gitignored. - The XML API requires a registered app token (
Authorization: Bearer, fromBGG_API_TOKEN) — unregistered requests get 401. The token is ACTIVE andtests/fixtures/bgg_cache/holds real recorded responses; add fixtures for new tests withscripts/record_fixtures.py. - Every stage is idempotent and resumable — killing mid-run and restarting must lose no work; re-runs skip already-processed items.
- Use only the XML API2 and the public website — no undocumented BGG endpoints (BGG tightened access policies in 2025).
- BGG has no write API: writes drive the real website with a logged-in Playwright session.
- Synthetic data must never reach upload. The stub era ended 2026-08-05:
matches.csvandtests/fixtures/bgg_cache/now hold real API data. The guard mechanism stays armed: the stub-fixture generators writedata/bgg_cache/STUB_FIXTURES.marker(gitignored) anddata/STUB_DATA.marker(committed), and the upload stage MUST refuse to run while either exists — regenerating stubs re-locks upload automatically.
Domain gotchas
- Base game vs. expansion vs. new edition is the top failure mode — bias matching toward
ambiguousover auto-match ("Wingspan Europe" must not match base Wingspan). - Editions/versions matter: Eric owns multiple editions of some games — each is a separate collection entry (keyed by
collidon BGG). Never guess a version: no legible cues →version_unknownand a version-less collection entry. - Normalize titles (casefold, strip punctuation/articles, special chars like é/&/:) identically on both sides of a match; dedupe across photos but keep
source_photosprovenance. - Human curation is durable:
data/title_splits.json(photo-scoped split-into-copies decisions, honored by extract's dedupe AND resolve's dedupe),data/title_edits.json(corrected reads/cues, applied before dedupe on every titles.json rebuild),data/title_removals.json(lines removed from the catalog — filtered out of every rebuild; delete the record to undo),data/title_additions.json(games added without a photo — joined into every rebuild; a later photo sighting dedupe-merges with them), anddata/local_games.json+data/local_art/(hand-written facts and a cover photo for off-BGG games — the ONLY source for them, merged over the photo reads by enrich) persist forever. Row-level decisions persist via thededupe_vetocolumn — edits never drop veto'd rows (a rename retitles them in place); removal drops them (explicitly discarding the line). - RPGs are local-only citizens: when the board-game search runs dry, resolve falls back to
type=rpgitem(same geekdo API/token). RPGGeek items carry their OWN link types (rpgdesigner,rpgpublisher,rpggenre,rpgcategory,rpgmechanic) — a board-game-only parser silently returns nothing for them. Matched rpgitems enrich into the library but diff routes them tolocal_only— they must never reachto_add.csv/upload (their collection lives on RPGGeek, out of scope). - Detailed BGG API behavior (202 queueing, collection-endpoint quirks, endpoints): use the
bgg-apiskill. If the spec's BGG behavior changes, update thebgg-apiskill to match — they must not drift.
Git
- Remote is self-hosted Gitea 1.26 (
git.kestrelsnest.social/eric/bggpipe), not GitHub —ghCLI does not work here. - Commit
data/matches.csv,data/to_add.csv,data/to_update.csv,data/upload_log.csv,data/titles.json,data/unidentified.json,data/unidentified_dismissed.json,data/title_splits.json,data/title_edits.json,data/title_removals.json,data/title_additions.json,data/local_games.json,data/local_art/,data/games.json,data/STUB_DATA.marker(while it applies), and the collection snapshot XMLs. Never commitdata/bgg_cache/,data/extract_raw/,photos/,data/.lan_key, Playwright storage state, or.env.