Commit Graph
53 Commits
Author SHA1 Message Date
Eric WagonerandClaude Fable 5 32b6aae841 Audit round 7, upload cluster: evidence over inference at every exit
Five blind reviewers swept the real-data-era surface; this lands the
upload findings, all verified against the code and the documented site
behavior before fixing.

The two HIGHs shared a root: logging outcomes the browser never proved.
add_game waited for an "Add To" button that an owned game's page does
not have — so a second-copy add could never succeed, and worse, an add
that LANDED but missed the log became an unretryable failure loop
(every retry: 30s timeout, logged failed, nothing ever settles).
add_game now polls for either button state: "In Collections" without
second_copy returns the previously-dead already_present status (the
landed-but-unlogged case heals itself on retry); with second_copy it
refuses loudly (that flow is unverified — add by hand). A save whose
dialog is slow to hide reloads the page and asks for ownership evidence
instead of guessing "failed". update_entry no longer trusts the editor
merely closing: the cell must settle on text matching the CHOSEN
version, else the AJAX save failed server-side and "updated" would
mark a job done forever that never touched the site.

Per-copy bookkeeping: stale_jobs endorsed per game, so rejecting one
of two queued editions let the rejected copy upload on the survivor's
endorsement — it now counts endorsements per (bgg_id, version) and
retires the game with "re-run diff" when a copy loses its backing.
annotate_queue stamped every row sharing a job key with the same log
status, so one success marked both vetoed duplicates done; completions
are now claimed one row per done log line.

Smaller findings: the version-drift note queued a doomed re-add after
warning about it (now skips — the entry exists on BGG; re-adding only
duplicates); the one-update-per-game deferral rested on a claim the
collid-exact editor disproves (removed — same-game updates run
together); the 3-identical-failures abort compared exception class
only, so three unrelated problems aborted a healthy run (now compares
whole messages).

Also from the test seat: run_upload's stale filtering finally executes
against a real matches.csv in tests; rejected credentials pin that no
anonymous storage state is saved; update_entry's three guarded exits
each have a test; _scrub's newline flattening is pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-06 00:21:52 -04:00
Eric WagonerandClaude Fable 5 9663f29702 Review can search a chosen database: BGG or RPGGeek, by hand
The automatic cascade only reaches RPGGeek when BGG's board-game search
comes up empty — so every D&D box, which BGG does list as board games,
can never find its RPGGeek entry no matter how many times it is
reopened. Eric hit exactly that and settled for keeping them local.

Match cards now carry an editable query with two buttons, "search BGG"
and "search RPGGeek", which replace the row's ballot with whatever the
chosen database returns (owned counts and ranks attached when
available; if that stats call fails the results still stand and the
degradation is reported). The TUI's (f) re-search falls back to
RPGGeek automatically when the board-game search is empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:58:58 -04:00
Eric WagonerandClaude Fable 5 50a710ce99 "Local" stops being a one-way door, and the head search stops skipping
Answering Eric's question — no, a local game never looks itself up again
— by making it possible. A local row's Titles line gains "look it up",
reopening it through the SAME cascade resolve uses (board games,
truncation heads, then RPGGeek) rather than the partial re-implementation
reopen_match had; that cascade is now one shared find_candidates()
instead of two drifting copies. Review's manual (f) re-search falls back
to RPGGeek too.

That exposed a real matcher gap: truncation heads jumped from
"drop the last word" straight to "first two words", so a printed title
that buries the real name in the middle was unreachable — "ALICE IS
MISSING A SILENT ROLE PLAYING GAME" never tried "ALICE IS MISSING".
Heads now shrink from the right, longest first (bounded at six, since
each is a rate-limited request); only exact normalized matches count for
heads, so shorter heads cannot match loosely.

Both of Eric's Alice Is Missing rows now find their RPGGeek entries
(311654, and 380459 for Silent Falls) and await his picks in review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:51:54 -04:00
Eric WagonerandClaude Fable 5 7e95ed607d RPGs pull real RPGGeek data; off-BGG games get facts and a cover photo
Two gaps at the edges of the library, both closed.

RPGGeek items live in the same database but use their own link types —
rpgdesigner, rpgpublisher, rpggenre, rpgcategory, rpgmechanic — so a
board-game-only parser found none of them and both RPG entries showed
just a year and a description. parse_things_full now reads both
vocabularies (plus rpgproducer/rpgseries): .dungeon gains John Battle
and Project Nerves, Parsely gains Jared A. Sorensen and its genres.

An off-BGG game has no API to enrich it and no publisher art to fetch,
so its detail page now hosts the only source it will ever have: a form
for title, year, players, playing time, publishers, designers and
notes, plus a cover photo upload. Both persist in data/local_games.json
and data/local_art/ (committed, like every other curation store) and
enrich merges them over the photo reads, so a rebuild can't erase them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:45:09 -04:00
Eric WagonerandClaude Fable 5 2e1693c0be Enrich's summary stops reporting a negative count
"136 entries (136 added/refreshed this run; -11 already present or
waiting)" — local library entries were counted in the same tally as API
fetches, but they have no API target, so the remainder went negative
once eleven off-BGG games existed. The two populations are now counted
and named separately, and zero-valued clauses are omitted:
"136 entries (0 fetched from BGG; 11 local-only; 125 already present or
waiting)."

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:32:55 -04:00
Eric WagonerandClaude Fable 5 b5e13be335 Count pending work: BGG's export lags, so raw queue rows lie
The card read "1 version updates" while upload said "skipping 1 already
done". Both were right. Recon on the live site shows the update DID
apply — the version cell reads "English first edition Year: 2012" and
its radio is checked — but BGG's XML collection export still reports
that collid with no version, even on a forced refresh. diff reads the
API, so it re-queued finished work; the log correctly refused it.

Nothing to fix in the flow: the pipeline card now counts PENDING jobs
(queue rows minus what the log completed) for both to_add and
to_update, reports outstanding failures rather than every failure ever
logged, and when everything queued is already applied it says so and
names the cause. Documented under "BGG's collection export lags the
site" so the next person doesn't chase it as a bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:29:25 -04:00
Eric WagonerandClaude Fable 5 e44c7e1b92 The Queue page reports what upload already did
All 36 version updates landed and all 36 still read as outstanding:
to_add.csv and to_update.csv are diff-time snapshots that never shrink,
and the page showed them without consulting the upload log. Rows now
carry their last attempt's outcome — pending / done / failed, plus
"retired" for jobs a later review decision withdrew — and each section
heads with a tally instead of a raw row count. A note explains that
finished rows persist until the next diff rebuilds the queue, and that
the log is the permanent record.

On Eric's data: to_update now reads 36 done, 0 pending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:23:50 -04:00
Eric WagonerandClaude Fable 5 0af9ae86c5 Updates go through the collection cell: collid- and version_id-exact
The last unverified flow failed on its guess — the collection row has no
link named "own"; the edit affordances are icon anchors with no text. The
site offers something far better, now verified: the row's VERSION CELL
carries its own collid in an onclick, and the inline editor it opens is a
radio list whose values ARE version ids.

So an update addresses the copy by collid and the edition by version id —
no name matching, no pagination, no dialog, and structurally incapable of
creating a duplicate entry (it sets one field on one collid). Clicking
the radio fires CE_SaveData itself; there is no Save button, and the save
has landed when the cell stops reading "Editing". A version id the editor
doesn't offer aborts with the entry untouched.

The class docstring's UNVERIFIED list is now empty but for the
second-copy add, which --verify already reports as a copy-count
shortfall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:15:39 -04:00
Eric WagonerandClaude Fable 5 74ecd5059b The retry badge stops offering jobs the human retired
Marking the D&D blue box "local" removed its job from the queue — but
the badge still counted its old failure and the checkbox still offered
to retry it, because the count read only upload_log.csv. A failure is
retryable only if the job is still queued AND still endorsed by
matches.csv; on Eric's data that's the difference between 2 and 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:06:40 -04:00
Eric WagonerandClaude Fable 5 99c1fbf02d Review decisions outrank the queue; stop waiting on OUR name for a heading
Two bugs behind one failure. Dungeons & Dragons timed out waiting for a
dialog heading matching our stored name — but BGG 140509's primary name
is "Dragones Y Mazmorras"; we matched it through an ALTERNATE name, so
that heading never appears. The add flow now waits for the Own checkbox
(the form itself) instead: /boardgame/<id>/ already establishes which
game the page is.

And the job should not have run at all. to_add.csv is a snapshot from
the last diff, so any review decision taken afterwards — local,
rejected, wrong-match — was invisible to upload. run_upload now
cross-checks every queued job against the CURRENT matches.csv and skips
those it no longer endorses, naming each and pointing at diff. When
matches.csv is absent or empty it condemns nothing: absence is not a
verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 23:02:59 -04:00
Eric WagonerandClaude Fable 5 6e72ef22d1 The picker's paging state outlives a reopen — navigate, don't assume
Sleeping Gods and Gloomhaven "vanished on the second pass" because the
second pass began wherever the first ended: closing and reopening the
version sub-view does NOT reset it to page 1 (Angular keeps the scope),
so the rescan started mid-list and never revisited the earlier pages
holding the row. Verified against the live picker.

The second pass now clicks the visible numbered "1" anchor first — and
so does the initial scan, since paging state can outlive anything. The
reopen is gone entirely. Docs record both this and the has_text
whitespace trap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 22:53:05 -04:00
Eric WagonerandClaude Fable 5 6f2f2dac53 Row clicks survive whitespace; the failure badge stops inflating
Sleeping Gods failed where three siblings passed: the second-pass row
click matched captured text with Playwright's has_text regex, which
tests raw textContent — tabs and newlines included — against a capture
that was whitespace-normalized. Rows whose markup happened to be tidy
matched; that one didn't. The picker now re-finds the row by NORMALIZED
text and clicks it by index, which also survives the list re-rendering
in a different order between openings.

And the UI's failure count only ever grew: upload_log.csv is an
append-only audit trail, so a retry that succeeds leaves its old
'failed' line in place. outstanding_failures() counts the LAST status
per job key — the same rule _plan_jobs already uses to decide what to
skip — so a landed retry clears the badge. On Eric's log: 6 'failed'
rows, 1 job actually outstanding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 22:49:02 -04:00
Eric WagonerandClaude Fable 5 9283a3e980 Paging clicks only visible controls: First/Prev are mobile-only
Munchkin Big Box and Tang Garden hung 30s each on a "First Page" anchor
that exists but is invisible: BGG renders every paging control twice,
and the First/Prev pair lives only in the mobile set
(<li class="visible-xs-inline">). A desktop viewport can never click it.

Paging now selects the first VISIBLE match, and returning to page 1
closes and reopens the sub-view (which always opens on page 1) instead
of reaching for a control that isn't there. A test proves no hidden
control is ever clicked — the fake picker raises if one is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 22:41:42 -04:00
Eric WagonerandClaude Fable 5 ba55863cef Version picker: real pagination, and matching that declines to guess
Five games uploaded; two landed version-less. Neither was the picker's
fault: paging is an AngularJS <ul class="pagination"> of anchors, not
buttons named "next", so the old guess found no control and quit after
page one — and BGG's API version names carry printing qualifiers the
picker omits ("English edition 2018-2" vs "(English edition) (2018)").

_select_version now scans the WHOLE list (verified selectors: rows are
<li>s with a thumbnail; a[title="Next Page"] advances; the parent <li>
disables at the end), collects every candidate, then decides: one exact
match wins; failing that, one match after stripping a trailing year
qualifier wins and says so; several matches are refused outright rather
than guessed, and the reason reaches upload_log.csv. Both call sites
carry the reason through.

docs/bgg-upload-flow.md records what the live site actually does —
including that every login-gate selector the doc called "verified" was
wrong, while the "unverified" dialog structure was right.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 22:36:48 -04:00
Eric WagonerandClaude Fable 5 6ae2667c58 First real upload lands: three browser-flow bugs, found by doing it
A Gentle Rain is on BGG (collid 148198034, version 701315 — the
English Bloom edition matched from a shelf photo), verified by
re-fetching the collection. The failures on the way, all in the
login gate the docs called "verified" and none in the version-picker
code they called "unverified":

1. BGG's Sign In is an <a class="btn"> with NO href, so it has no
   implicit link role: get_by_role("link", name="Sign In") matched
   zero elements in EVERY state, and "no Sign In link" was read as
   "already signed in". Every run browsed anonymously.
2. The header hydrates after domcontentloaded, so for a moment
   neither control exists — a check resting on one absence guesses.
   _signed_out() now polls until the page proves one state or the
   other (Sign In vs Sign Out) and raises after 30s rather than
   assume; login is verified by the transition, and the session file
   saves only after that proof.
3. get_by_label("Own") also matched "Prev. Owned" — strict-mode
   violation; the Own checkbox is now matched exactly.

Also: Playwright's multi-line call logs no longer break upload_log.csv
into ragged rows (errors flatten to one line). Three unit tests cover
the hydration window, positive detection, and the undeterminable case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 22:22:11 -04:00
Eric WagonerandClaude Fable 5 481fa63488 "Wrong game" re-searches on the spot; resolve explains what it won't do
Eric pressed wrong-game on both rows, ran resolve, and resolve
truthfully found "nothing to match" — unmatched is a human-owned state
it refuses to touch, but the button's name promised a re-match and the
pipeline was the natural place to seek one. reopen_match now clears
the match AND re-searches immediately (merged depunct search, sibling
editions included, stats attached), returning the row as an ambiguous
ballot on the very card the button lives on; if BGG is unreachable it
reopens bare with a visible warning, and re-search/manual-id remain.
run_resolve's summary now says out loud that unmatched rows wait for
the human — it never overrides a decision, including "this is wrong."

Data: the two stranded WIZ-WAR rows re-searched into ballots — both
cards now offer all three lineages, awaiting Eric's picks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 21:53:22 -04:00
Eric WagonerandClaude Fable 5 1dd72d2688 The matcher stops trusting what the user can't see
Eric's question cut to the bone: "How would a user know? It matched
wiz-war and that IS the game." The auto looked unanimous because the
matcher discarded the evidence of doubt before anyone saw it — and
worse, BGG's search hides evidence of its own: results truncate
unordered in the several-hundreds (the game named "Dungeon!" appears
in NEITHER the "Dungeon!" nor the "Dungeon" search), and punctuation
can bury matches.

Three matcher changes: every title is searched raw AND depuncted,
merged by id; a name that becomes exact once its trailing
parenthetical is stripped ("Wiz-War (Eighth Edition)") is a sibling
edition — BGG files new editions as separate games — and enters the
candidate set at exact grade, so same-named lineages land in review as
a visible choice; and a LONE candidate must now earn trust (stats
fetched, sibling-grade never autos alone, true exacts must clear the
dominance ownership floor) — closing the fast path both impostors
(.dungeon at 31 owners, then Dungeon (ICP)) walked through.

Recorded outcomes: WIZ-WAR → ambiguous with all three lineages on the
ballot; Dungeon! → ambiguous (its true match is beyond BGG's search
horizon — that's what manual id is for); every legitimate auto in the
fixture set held. And the answer to Eric's second question is now
structural: re-match never re-decides — it demotes to unmatched and
the HUMAN picks from re-search or manual id; the machine only chooses
on first resolve, and it now chooses more humbly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 21:47:49 -04:00
Eric WagonerandClaude Fable 5 8db69685c4 Version cards get the wrong-game escape hatch; scroll stops yanking
Eric stared at game 589's complete printing list hunting for boxes
BGG files as SEPARATE games — the escape (wrong match, on the Titles
edit panel) was two pages from where the dead end happens. Version
cards now carry "wrong game — re-match", posting the existing
reopen-match endpoint: the card moves to the Matches section where
re-search and manual id live.

And highlight() scrolled the active card into view on EVERY render —
for a mouse user the cursor idles on card one, so every button click
yanked the page to the top. Scrolling now happens only on keyboard
moves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 21:37:35 -04:00
Eric WagonerandClaude Fable 5 3e61a03686 Catalog pairs same-title rows by photos, not csv position
The Wiz-War lines were displaying each other's rows: an edit re-queue
recreates its row at the END of matches.csv, and the catalog's
positional per-title pairing then crossed the wires — the 4504 line
wore 4528's open ballot while 4528's line offered 4504's pick-edition
button (whose click re-targeted by photos and safely hit the other
row, deepening the confusion). Pairing now matches run_resolve's rule:
exact photo set, then overlap, then positional fallback, with
unclaimed rows appended as their own lines. Plus a regression test
with deliberately reversed csv order, and the open-ballot marker
restyled as a quiet dotted link instead of a mis-wrapped button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 21:32:46 -04:00
Eric WagonerandClaude Fable 5 c94e8bc695 Confirmations pin to the viewport; open ballots mark their rows
Twice the feedback was the bug: the success banner lives at the top of
the page and the user acts at the bottom of a 136-row list — so a
working "pick edition" read as broken (it worked both times; the data
proves it). showToast() pins transient confirmations to the viewport
bottom (role=status, carries buttons, auto-dismisses), and pick-
edition/edit-saved use it. Better: state stops being transient at all —
any row whose edition ballot is open shows a persistent "ballot in
Review" link where its button was, so the answer to "where did it go?"
lives on the row forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 21:25:49 -04:00
Eric WagonerandClaude Fable 5 edb72514f4 The quiet successes learn to speak
Eric pressed "pick edition", the button vanished, and nothing said
where the ballot went; he edited a matched title and its row silently
entered re-resolve limbo until a resolve run nobody knew was owed. Now:
pick edition banners a link to the Review ballot it created; saving an
edit banners the re-queue contract with an inline "run resolve now"
button; and the waiting chip says "awaiting resolve" (its old label,
"awaiting BGG", described the token era). Data-wise this commit also
carries the re-resolved IMG_4504 Wiz-War copy — whose 2012 year cue
correctly matched NO version of game 589, the breadcrumb that it
belongs to the FFG entry (104710).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqdWSz9g
2026-08-05 21:16:16 -04:00
Eric WagonerandClaude Fable 5 ab28476741 Version ballots drop the silent eight-candidate cap
resolve_version truncated the plausible list to eight — a version
ranked ninth was silently unreachable, distinguishable from absent
only by API spelunking. Every plausible version now ships (the ballot
sorts by score, so the tail costs nothing), and version cards gain
"list every printing", wired to the existing open-versions endpoint,
for when even the plausible set doesn't hold the right box — cues can
mislead as well as shortlist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 20:40:05 -04:00
Eric WagonerandClaude Fable 5 c5daad023d Same-title copies become tellable apart; wrong autos become fixable
Three split Wiz-Wars meant three identical "Wiz-War" edition cards —
version cards now carry their copy's photo thumbnails and name the
source photo in the prompt, so each ballot says which box it's about.

And the .dungeon-class failure gets its escape hatch: "wrong match" on
any matched Titles row clears the match and returns the title to
Review's unmatched queue, where re-search and manual-id already live.
Help notes the BGG wrinkle that makes this matter for Wiz-War
specifically: early editions and the FFG remake are SEPARATE games, so
a copy whose edition isn't on the ballot belongs to the sibling entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 19:49:19 -04:00
Eric WagonerandClaude Fable 5 42ef34c3a7 Review cards sort alphabetically within each section
Same reasoning as the Titles page: csv order is extraction order,
which reads as random. Matches, Editions, and Merges each sort by
title (case-insensitive); the sort is stable within a payload so the
keyboard cursor doesn't jump between polls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 19:20:15 -04:00
Eric WagonerandClaude Fable 5 90e75daf98 Add a game by hand: the fourth curation store
BGG wants base game and expansion as separate collection entries, but
a box that stores its expansion's bits shows one spine to the camera —
the hidden half was unreachable. "add a game" on the Titles page
records an entry in data/title_additions.json (committed, like every
curation store), joined into every rebuild BEFORE edits and dedupe: so
corrections apply to it, a later photo sighting of the same game
merges instead of duplicating (photo provenance wins), and re-adding
an existing title is a no-op. Photo-less lines show an "added by hand"
chip where their photo links would be; from resolve onward they are
ordinary titles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 19:13:49 -04:00
Eric WagonerandClaude Fable 5 7b6ea90934 Job status classes get an s- prefix: .done was the celebration card
"resolve: done" rendered <span class="done"> — which is the review
page's all-done celebration card class, so the status line became a
floating bordered card squatting on the Activity heading. Status
spans are now s-idle/s-running/s-done/s-failed, scoped under
#jobstate, and done earns its green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 19:02:39 -04:00
Eric WagonerandClaude Fable 5 e187ed4f3f Off-BGG games become local library citizens
An unmatched title that's a REAL game BGG doesn't have dead-ended:
manual id or reject. The RPG local-citizen pattern generalizes to a
human decision — review (web + TUI, key l) gains "not on BGG — keep
locally": match_status "local" clears any BGG identity, diff routes it
to local_only (never queued), and enrich synthesizes a library entry
from the game's own photo reads (name, year, publisher cue — no API
call, so even a blocked run lands them; pruning keeps local keys).
Library and Titles show a "local — not on BGG" chip; Help's legend,
review description, and shortcuts cover the new verb, distinguishing
it from reject (bad read / not a game).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 18:59:56 -04:00
Eric WagonerandClaude Fable 5 59f4b8c43c The real-data era: token live, stubs retired, editions on demand
BGG application approved. The migration the stub markers guarded for
weeks: both synthetic caches deleted; tests/fixtures/bgg_cache
re-recorded from the live API (recording list extended to every
scenario the suite exercises — Civilization truncation, the Sorcerer
SPI tiebreak, StarForce, Flat Top's thematic year, Alice Is Missing's
rpgitem fallback); resolve --force re-matched all 133 titles for real
(109 auto, 6 ambiguous, 18 unmatched, 30 edition ballots);
data/STUB_DATA.marker deleted with its exit condition met — the guard
mechanism stays armed should stubs ever regenerate.

Reality fixed one bug and taught one lesson. The bug: a multi-type
search lists an expansion twice (once per matched type) and the parser
kept the generic boardgame entry — parse_search now dedupes by id
preferring the specific type, which is what keeps expansion tagging
(the base-vs-expansion review guard) alive on real data. The lesson:
hand-built ambiguity is tidier than the real thing — Wingspan has 46
versions with three plausible English Stonemaier printings, so the
suite's synthetic version ids and version_auto expectations became
real ballots (assertions updated to recorded reality; the cue-plumbing
test keeps its crafted two-version scenario via an injected
transport).

New: pick edition. A cue-less matched row is version_unknown by design
(never guess) — but the owner knows which printing the box is.
open_version_ballot() fetches the game's complete version list,
cue-scores it when cues exist, and marks the row version_ambiguous so
the normal Review edition pass presents it; the Titles page grows the
button (Eric's three Wiz-Wars: two cue-less copies can now each claim
their edition).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-05 18:50:17 -04:00
Eric WagonerandClaude Fable 5 be8e19b418 Docs: the phone flow, in all three places it lives
README gains the home-screen tip and the camera-to-pipeline flow with
its shelf-<timestamp> naming; Help's Photos description now speaks
phone (and clarifies that only same-FILE-NAME re-uploads trigger the
reshoot replacement); CLAUDE.md documents the --lan architecture (key
file, QR pairing, guard exemptions, camera-name minting) and adds
data/.lan_key to the never-commit list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-03 17:18:04 -04:00
Eric WagonerandClaude Fable 5 cf6dd5e134 Photo upload gets a visible lifecycle
A phone photo is several MB — seconds of dead air in which the user
reasonably assumes it worked and moves on, losing the upload if they
navigate or lock the phone (exactly how a photo went missing today).
The dropzone now narrates: uploading with count and size ("keep this
page open"), a green ✓ naming each saved file, reverting after a few
seconds; failures keep their alert but also reset the zone. A
beforeunload guard makes the browser ask before abandoning an
in-flight upload, re-picking the same file re-fires, and the gallery
refresh is forced so the new photo appears immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-03 17:10:13 -04:00
Eric WagonerandClaude Fable 5 f82489716a --lan key persists across restarts; stale tabs get a readable 403
The key rotated on every server restart, stranding every phone that
held the previous cookie — during active development that guaranteed a
wall of refusals from stale polling tabs after each restart. The key
now lives in data/.lan_key (gitignored, 0600 — the Playwright-state
treatment for credential-adjacent files) and is reused across
restarts; delete the file to rotate. A keyless browser navigation now
gets a one-line HTML page saying what to do instead of raw JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-03 16:39:24 -04:00
Eric WagonerandClaude Fable 5 344a930a8a --lan first load: redirect commits the key cookie; static goes public
The phone's first visit 403'd its own subresources: Safari's preload
scanner fetches /static/* before the document response's Set-Cookie is
committed, and favicon/apple-touch-icon probes are cookie-less system
fetches. A ?k= visit now answers 303-with-cookie to the same path —
the cookie is committed before any document loads, and the key is
scrubbed from the phone's address bar and history. /static/* and the
icon probe paths are exempt from the key: they're the app's own
css/js/artwork, no user data (shelf photos stay gated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-03 16:31:57 -04:00
Eric WagonerandClaude Fable 5 95fd18ca5c --lan startup: lead with the default-route address, demote the rest
A multi-homed machine (VM bridges, Ethernet + Wi-Fi) has several
addresses and the server cannot know which network the phone is on —
but the OS's default route is the right answer nearly always. The
banner now prints one "on your phone" URL from the route probe, with
the other interfaces on an if-that-doesn't-answer line; when the probe
fails, the settings hint plus candidates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-03 16:21:26 -04:00
Eric WagonerandClaude Fable 5 429c519237 Proofread pass: 11 edits, 11 removals from the Titles page
Eric's curation session through the new UI — corrected reads and cues
(Dungeons & Dragons, Huggermugger, Patch Work, Verdant, Wiz-War, the
Ain't It Cool Trivia Game, and others) and removed non-game or
duplicate-read lines (publisher spines read as titles like Slugfest
Games and Cheapass Games, a misread "Bird on Your Bread?!", stray
partial reads). titles.json reflects the replayed decisions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-03 15:20:32 -04:00
Eric WagonerandClaude Fable 5 f19b861251 The proofread checkpoint gets its place: nav order, badge, and a rename
Eric's observation: cleaning up raw reads happens BEFORE resolve, but
the app ordered Review ahead of Catalog and never said proofreading was
a step at all. The pipeline has two human checkpoints — proofread the
reads (after extract), decide the matches (after resolve) — and now the
app says so: the sidebar runs Pipeline, Photos, Titles, Review, Queue,
Library, Help in true workflow order; the Titles page (né Catalog — the
old name suggested a finished collection, which is the Library's job;
/catalog redirects) gets a badge counting unresolved shaky reads; an
edit marks its entry human-verified so the badge drains as you
proofread; the extract stage card nudges toward the proofread before
resolving; and the Help flow is rewritten as six stages + two
checkpoints with the loop called out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-03 00:19:59 -04:00
Eric WagonerandClaude Fable 5 626f255c01 Remove-from-catalog: a third durable curation store
Titles that aren't games (misread box art, out-of-scope items) can now
be removed outright: a danger button in the catalog's edit panel posts
/api/remove-title, which drops the line's matches rows (veto'd ones
too — removal is the human explicitly discarding the line), records the
decision photo-scoped in data/title_removals.json, and replays
titles.json. Every rebuild filters removed sightings after edits and
before dedupe, so re-extraction cannot resurrect them; undo by deleting
the record from the store. The three stores now share one scoped-record
parser and recorder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-02 20:15:46 -04:00
Eric WagonerandClaude Fable 5 86d434a400 Audit round 5 (curation feature): 5 blind reviewers, 14 confirmed fixes
The standing post-feature audit over a7f0cfe. Correctness (data): splits
become photo-scoped store records so splitting one edition no longer
force-splits same-named editions, and renaming a split copy migrates its
protection to the corrected title instead of silently re-merging copies.
Correctness (web): edit scoping now counts siblings by NORMALIZED title
(matching how stored edits apply), same-title-same-photos edits are
refused rather than corrupting the sibling entry, split copies serve
their real per-photo cues to the edit form instead of blanks, and a
split whose row vanished underneath returns 409 instead of a false 200.
Silent failures: replay_titles refuses to rebuild from a PARTIAL raw
cache (fresh clone + one --only extract would have truncated the
committed titles.json); the edit endpoint writes in crash-safe order
(cull, record, replay); corrupt curation stores fail loud naming the
file; retried edits don't double-record. Review-decision durability:
drop_rows never drops dedupe_veto rows — a rename retitles them in
place — and writes through a no-reload path so a concurrent rewrite
can't silently discard the cull. Style: catalog action cells get their
own class (.rowactions' flex display broke table alignment), editor
inputs match the design system and stop overriding the global
focus-visible outline, EditBody's clear-semantics docstring scoped to
cue fields, "nothing to change" derived from the record itself.

Tests: 8 new (photo-scoped splits, veto preservation, photo-narrowed
drops, 409s on both curation endpoints under a running job, partial-raw
replay guard, rename-keeps-protection lifecycle, corrupt-store error,
cue-field editing) and the dead edition_hint key in the edit test now
exercises real cue fields. 259 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-02 20:04:15 -04:00
Eric WagonerandClaude Fable 5 a7f0cfee05 Durable curation: persisted splits + pre-resolve title edits, catalog A→Z
Wiz-War had no split button: can_split required a matches row, but fresh
extractions leave multi-photo titles rowless until resolve runs. Splits
are now a title-level decision persisted in data/title_splits.json,
honored by extract's dedupe and resolve's dedupe on every rebuild, with
the button on any multi-photo line — resolved or not.

Same mechanism carries human corrections: data/title_edits.json stores
fixed misreads and known cues (publisher/edition/year/language), applied
before dedupe on every titles.json rebuild, editable from a new inline
form on every catalog line. An edit drops the title's stale matches rows
so resolve re-queries with the corrected data.

The catalog page now sorts alphabetically (case-insensitive; split
copies stay adjacent) instead of extraction order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-02 19:47:08 -04:00
Eric WagonerandClaude Fable 5 24a7bcb9e0 Fresh extraction of all 56 photos with the current pipeline
Full artifact reset and re-extraction: 132 titles (was 133), 62 reshoot
sightings, 17 titles resolved from stub cache, 115 parked for the
token. Several old misreads self-corrected on the fresh pass
(Hebarceos->Herbaceous, THE BEST OF RA...->CHEAPASS GAMES, Mostly
Original->The Ain't It Cool Trivia Game) and box subtitles now read
fuller, which shifts many stub cache keys — those titles wait for real
data rather than resolving against stale stub queries. Review state
starts clean by design; splits and dismissals are Eric's to redo in
the UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 19:28:05 -04:00
Eric WagonerandClaude Fable 5 851b34e369 Split into copies: the symmetric partner of the merge veto
Three identical boxes in three photos are indistinguishable from one
box photographed three times, so extract's dedupe folds them into one
entry — correct for overlapping shots, wrong for a shelf holding three
editions of a favorite game. The catalog now offers "split into copies"
on multi-photo rows: the row explodes into one row per photo, each
dedupe_veto-flagged so no future resolve re-merges them, each keeping
its match but reopening its own edition slot (candidates preserved when
present). Resolve's provenance-follow skips split rows (their photo
sets are human-authored), the catalog renders surplus split copies as
their own lines with a "copy" chip, and diff's vetoed-duplicate logic
turns them into the extra collection entries they are.

Applied to the real data: Wiz-War is now three copies across IMG_4502/
4504/4528 — one claims the owned collection entry, two queue as new
second-copy adds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 19:10:26 -04:00
Eric WagonerandClaude Fable 5 74fc4fe847 RPGs become local library citizens: identified, enriched, never uploaded
When the board-game search (and truncation heads) runs dry, resolve
falls back to type=rpgitem — the geekdo database is shared, so the same
API, token, cache, and classification machinery apply. Matched rpgitems
flow through review and enrich normally but diff routes them to a
local_only bucket, structurally outside to_add/to_update: their
collections live on RPGGeek, beyond this pipeline's write scope. The
library page gains an All/Board games/RPGs filter and an "RPG · local
only" badge; the catalog tags them too. Fixture generators write blanket
empty rpgitem stubs for every known query (the fallback fires for every
unmatched title) with real synthetic entries for Alice Is Missing.

Data: both Alice rows re-resolved from unmatched to auto rpgitem
matches. First diff since the audit reworks also lands their real-data
consequences: Dungeon! gains its TSR edition update on a versionless
copy the old claim ordering missed, to_add rows carry unioned reshoot
provenance, and the Herbaceous typo row's survivor is now the
correctly-spelled title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 19:01:09 -04:00
Eric WagonerandClaude Fable 5 8e3cdc145f Docs for strangers: real quick start, own-shelves guide, clone-safe stub guard
README rewritten for a fresh clone: working status, quick start with
env-var table and per-stage commands, photo-taking guidance, a
bring-your-own-shelves section (this repo carries the author's live
data), tokenless workflows, and a dev section. New committed
data/STUB_DATA.marker closes a real gap: the cache marker is
gitignored, so a fresh clone had stub-derived to_add.csv with no
guard — upload now refuses on either marker. Username docs: BGG_USERNAME
in .env is the one place it's needed; config.toml is a fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 13:33:52 -04:00
Eric WagonerandClaude Fable 5 96637cfa7f Extract IMG_4566: Alice Is Missing base + Silent Falls expansion
Both resolve as unmatched by design — it's an RPG (geekdo rpgitem), so
the boardgame search realistically returns nothing; empty-search stubs
cover all five queries incl. truncation heads. Resolve also merged six
duplicate reads across photos (Hebarceos -> Herbaceous et al.).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 13:17:50 -04:00
Eric WagonerandClaude Fable 5 6da499d31a Review UI: surface the unresolved backlog and a full catalog ledger
The done screen claimed "diff-ready" while 104 extracted titles had
never been resolved (they're invisible to matches.csv until the BGG
token arrives). The state now counts titles.json entries with no
matches row: the header tally shows "awaiting resolve", the done screen
says "Resolved set fully reviewed" with the real extracted total and
what to run on token day, and a read-only Catalog section lists every
extracted title with its status chip (auto/approved/rejected/awaiting
BGG), matched game, version, and source photos.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 16:45:57 -04:00
Eric WagonerandClaude Fable 5 f574fb2b3c Extract 15 reshoot photos: +15 titles incl. resolved misreads
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 16:40:05 -04:00
Eric WagonerandClaude Fable 5 e03f407193 Extract 39 new shelf photos: 115 unique titles, 29 reshoot tickets
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 16:02:13 -04:00
Eric WagonerandClaude Fable 5 4bf7481f9b Extract reports unidentifiable boxes for human retakes
The vision prompt now returns {titles, unidentified}: boxes that look
like games but can't be confidently titled are reported (location
relative to identified neighbors, partial text, art notes) instead of
silently omitted. They land in data/unidentified.json keyed by photo,
and the end-of-run summary lists them — plus low-confidence reads —
with instructions to retake a closer photo and re-run. New --force flag
re-extracts everything; pre-feature raw caches (bare arrays) still
parse. Live run on IMG_4499 confirmed the flow and the low-confidence
list correctly flags the known "Hebarceos" misread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 14:24:16 -04:00
Eric WagonerandClaude Fable 5 58956f626d End-to-end run: extract -> resolve -> review -> diff on real data
27 titles from 4 shelf photos against the real 2018 collection snapshot:
19 already owned, 8 to add (2 with confident versions), 9 additive
version updates for hand-entered version-less entries. Review rescued
the "Hebarceos" vision misread via re-search and settled Wiz-War as the
Eighth Edition; Dungeon!'s three same-publisher TSR editions correctly
stay unresolved rather than guessed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 14:15:20 -04:00
Eric WagonerandClaude Fable 5 4a466f2a68 Progressive title truncation, publisher tiebreak, thematic-year fix
Long transcribed box titles that defeat search now retry with shorter
heads (pre-separator, pre-"Game ..." descriptor, first-two-words from
the pre-subtitle part) matched exact-only against the head — fuzzy
thresholds stay untouched. Tie-breaks gain a publisher pick: when the
box showed a publisher and exactly one exact-named candidate is from
that publisher, it wins (SPI's Sorcerer 1975 now beats the more-owned
White Wizard Sorcerer 2019). The extract prompt excludes thematic/
subject years from year_hint; re-extracting Flat Top's photo drops the
bogus 1942, and a regression test pins that a wrong year can never
drive version selection. Re-extraction also drifted two transcriptions
(DUNGEON!, and Herbaceous misread as "Hebarceos") — fixtures added; the
misread demos review's re-search rescue in the end-to-end run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 14:14:15 -04:00
Eric WagonerandClaude Fable 5 09274e039c Photo-title fixture set + resolve baseline; language-only cues skip fetch
Data-driven stub XML for all 27 extracted titles (searches incl. empty
results for the long Avalon Hill/SPI box titles, truncated-head retry
queries for later, tie-break stats with publisher links, version lists
aligned with the real collection objectids). Language-only cues no
longer trigger a versions fetch — language can't reach the plausibility
threshold alone, so the request would always be wasted; it still scores
when stronger cues exist. Baseline resolve: 22 auto, 1 ambiguous
(Wiz-War editions), 4 unmatched (the long box titles — next commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 14:09:13 -04:00