Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
bd0515d
docs(specs): draft spec 082, scanner capture always in view (#344)
plainprogrammer Nov 13, 2026
a6a8173
docs(specs): revise spec 082 after the spec review (0.2.0)
plainprogrammer Nov 13, 2026
96c7981
docs(specs): revise spec 082 after the second review (0.2.1)
plainprogrammer Nov 13, 2026
5882601
docs(specs): approve spec 082 at Gate A with bar A (#344)
plainprogrammer Nov 13, 2026
3e07d3b
docs(specs): spec 082 patch 1.0.1, the notice partial shared with spe…
plainprogrammer Nov 13, 2026
f5c8d95
docs(specs): record the maintainer's ruling on spec 082's notice part…
plainprogrammer Nov 13, 2026
dee3590
docs(specs): spec 082 patch 1.0.2 and plan (#344)
plainprogrammer Nov 13, 2026
78c9225
feat(scanner): the live guide's geometry (#344)
plainprogrammer Nov 13, 2026
86c39f6
test(scanner): draw the synthetic card where the live guide falls (#344)
plainprogrammer Nov 13, 2026
43ca9dc
refactor(layout): the notice's lines in one partial (#344)
plainprogrammer Nov 13, 2026
3ecc3b5
feat(scanner): the destination, the list's title and the after-add se…
plainprogrammer Nov 13, 2026
9c70cf2
feat(layout): a focused page's body, with no header, status or footer…
plainprogrammer Nov 13, 2026
8d4ef22
feat(scanner): the one-screen shell's markup (#344)
plainprogrammer Nov 13, 2026
28859af
feat(scanner): the shell's state, its sheets and the live guide on th…
plainprogrammer Nov 13, 2026
6f590c0
feat(scanner): capture freezes, a result opens the pane, Scan again r…
plainprogrammer Nov 13, 2026
33b110d
feat(scanner): the sitting's answers write the new regions (#344)
plainprogrammer Nov 13, 2026
1d48f9e
feat(scanner): focus after a redraw on the one-screen page (#344)
plainprogrammer Nov 13, 2026
8233d05
test(scanner): the sitting on the one-screen page (#344)
plainprogrammer Nov 13, 2026
429ea0b
feat(scanner): the flash and the end of a sitting on the after-add li…
plainprogrammer Nov 13, 2026
eee51d7
fix(scanner): a refused Undo also shows on the after-add line (#344)
plainprogrammer Nov 13, 2026
b9fc02c
test(scanner): the touch floors on the one-screen page (#344)
plainprogrammer Nov 13, 2026
33278a4
test(scanner): the stage notices on the one-screen page (#344)
plainprogrammer Nov 13, 2026
910c9cd
refactor(css): drop the scanner rules the one-screen page replaced (#…
plainprogrammer Nov 13, 2026
d962492
feat(scanner): a measured run per framing (#344)
plainprogrammer Nov 13, 2026
6422ad8
feat(scanner): measurement mode on the shell, with the framing in the…
plainprogrammer Nov 13, 2026
7d36c47
feat(scanner): the paired re-measure's findings (#344)
plainprogrammer Nov 13, 2026
37b0d42
test(scanner): a reading lands in the full framing's run by default (…
plainprogrammer Nov 13, 2026
231c8b8
docs(architecture): the one-screen scanner (#344)
plainprogrammer Nov 13, 2026
855c786
docs(design-system): the scanner page as built (#344)
plainprogrammer Nov 13, 2026
92d0c89
docs(specs): spec 082's supersessions in the index (#344)
plainprogrammer Nov 13, 2026
cf5b079
docs(specs): spec 082 screenshots and GIFs (#344)
plainprogrammer Nov 13, 2026
5784103
Merge origin/main (spec 084) into 082-scanner-capture-always-in-view …
plainprogrammer Nov 13, 2026
c4cbfe7
docs(scanner): summaries on the two changed scanner helpers (#344)
plainprogrammer Nov 13, 2026
feb9a5a
test(footer): the licence line ends the scanner's sheets' foot, not a…
plainprogrammer Nov 13, 2026
4a737f4
test(footer): the scanner's sheets and noscript notice carry the lice…
plainprogrammer Nov 13, 2026
fba660b
docs(specs): spec 082 verification record (#344)
plainprogrammer Nov 13, 2026
7783f23
fix(scanner): the after-add line's action, the failure block and the …
plainprogrammer Nov 13, 2026
85469a3
test(scanner): hold the reading in the AC-5.4 example so the status l…
plainprogrammer Nov 13, 2026
bf5824c
fix(scanner): empty live regions stay rendered and take no space (spe…
plainprogrammer Nov 13, 2026
f0a2c4f
fix(scanner): a refused Set redraws the top bar and the sitting too (…
plainprogrammer Nov 13, 2026
502c6a9
fix(scanner): a sheet's live regions are emptied on opening, so an Un…
plainprogrammer Nov 13, 2026
e1390c6
fix(scanner): a photo picked while a result shows clears the result a…
plainprogrammer Nov 13, 2026
bdc24ac
fix(scanner): a capture drops the alert style the after-add line was …
plainprogrammer Nov 13, 2026
423301e
fix(scanner): the pane's sentence no longer replaces measurement mode…
plainprogrammer Nov 13, 2026
fc22bb4
fix(scanner): a framing sent as a hash or a list answers 422, not 400
plainprogrammer Nov 13, 2026
f1b360a
fix(scanner): the broken-manifest page carries the small print in its…
plainprogrammer Nov 13, 2026
7bfd66d
docs(scanner): stale comments and docs match the built page (spec 082)
plainprogrammer Nov 13, 2026
fa4bf23
test(scanner): the paired report states its run conditions and bar A'…
plainprogrammer Nov 13, 2026
dfa4429
test(scanner): the top bar's order and a zone's label are proved on t…
plainprogrammer Nov 13, 2026
03e995f
test(scanner): the page's layout and the result pane's failures are p…
plainprogrammer Nov 13, 2026
ec8a660
test(scanner): the sitting's top bar, the after-add line and measurem…
plainprogrammer Nov 13, 2026
e977d11
test(scanner): a flash's Undo sits beside the after-add sentence, out…
plainprogrammer Nov 13, 2026
f7b19c1
fix(scanner): the alert style leaves the after-add line when a later …
plainprogrammer Nov 13, 2026
c896cf6
test(scanner): engine loading, a result with Other printings and the …
plainprogrammer Nov 13, 2026
a79bdfe
docs(specs): spec 082 verification after the implementation review (#…
plainprogrammer Nov 13, 2026
aa88afe
chore(memory): capture spec 082's session learnings (#344)
plainprogrammer Nov 13, 2026
e2e3598
docs(architecture): distil spec 082 into the scanner and catalog-core…
plainprogrammer Nov 13, 2026
2879cb0
docs(specs): mark spec 082 frozen in part by the scanner and catalog-…
plainprogrammer Nov 13, 2026
e9eecf3
docs(specs): record spec 082's distillation and the coordinator's che…
plainprogrammer Nov 13, 2026
e889e83
Merge origin/main (specs 081 and the 084 and 081 plan retirements) in…
plainprogrammer Nov 13, 2026
14b9d84
test(scanner): wait for the held reading before releasing it (#344)
plainprogrammer Nov 13, 2026
32d3d6d
test(scanner): measure each sheet at rest and on its own in the touch…
plainprogrammer Nov 13, 2026
0fc9960
docs(specs): spec 082 1.1.0, the paired re-measure withdrawn by the m…
plainprogrammer Nov 13, 2026
3786bc3
Merge origin/main (specs 085, 086, 087, the #404 fix, plan retirement…
plainprogrammer Nov 13, 2026
8754936
docs(specs): spec 082 research, the sitting at the full framing recon…
plainprogrammer Nov 13, 2026
9e1aed1
docs(scanner): the withdrawn re-measure in the area doc, comments, in…
plainprogrammer Nov 13, 2026
57e3318
docs(specs): spec 082's frozen line covers the whole spec (spec 027 D4)
plainprogrammer Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/memory/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- [Third-party fault reproducers stay out of the suite](third-party-fault-reproducers-stay-out-of-the-suite.md) — a reproducer asserting a library's fault is the failing-first proof only; code goes in the issue and commit body
- [Pre-push CI evidence](pre-push-ci-evidence.md) — a passing pre-push hook prints nothing; cite the pushed SHA, count examples with bin/rspec --dry-run
- [Turbo Back navigation quirks](turbo-back-navigation-quirks.md) — frame-advance has two races (the late visit cancels Back or a click, issue #62); filters now plain Drive GET visits + no-cache; stuck full-width progress bar = cancelled visit; Back within one frame of a render loses the saved scroll (#404); wait_for_turbo_idle, wait_for_scroll_events
- [Turbo Stream strings are html_safe](turbo-stream-string-is-html-safe.md) — turbo_stream.update(target, "text") inserts the string as HTML; escape card and place names with ERB::Util.html_escape or use a partial (spec 082)
- [PR #5 follow-ups](pr5-open-followups.md) — back-button flake fixed by PR #9 (merged 2026-10-02), watch CI; uncaptured failure
- [Card scanner direction](card-scanner-direction.md) — spec 011 executed 2026-10-08 (34/35 right card first, margin 300 kept by ruling); next = sdd-review Mode B, then finishing
- [Shipped percentile is upper-middle](shipped-percentile-upper-middle.md) — ScannerFindings.percentile(…, 50) takes the upper middle for even n; use one conventional median helper and state it
Expand Down
1 change: 1 addition & 0 deletions .claude/memory/headless-firefox-narrow-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ Related: [[pr-screenshots-workflow]].
- **Keyboard-only selects (spec 041, 2026-11-06):** under WebDriver in headless Firefox, arrow keys don't change a closed `<select>`'s value (its `selectedIndex` stays 0 whether or not the list opens). A keyboard-only example chooses by type-ahead instead: focus the select and `send_keys` the option's first letter.
- **Scrollbars are pinned to classic (#271, 2026-11-10):** on a Mac, Firefox follows "Show scroll bars: Automatic" and draws overlay scrollbars (no width) when no mouse is connected, which made a 360px frame 15px wider than CI's and broke a wrap check locally only. `spec/support/system.rb` now sets `ui.useOverlayScrollbars = 0`. A driver registered under another name (the dark-theme ones) needs the same pref if it opens a narrow frame. See `docs/lessons/2026-11-10-a-local-only-failure-was-the-macs-scrollbar-setting.md`.
- **A 360px frame is not a 360px phone (spec 069, 2026-11-11):** the frame keeps the forced classic scrollbar (15px on the Mac, about 12px on CI) and the body has an 8px margin, so a 360px frame's page container is 297px (360 − 15 − 16 body − 32 padding) and a deck-shape panel's interior 263px; a real 360px viewport gives a 312px container and a 278px interior. Model a phone with `open_in_viewport_frame` (a 360px viewport; a 375px frame is only right where the scrollbar is 15px). The `@container page` breakpoints (640px, 992px) are on the container (frame − 15 − 16 − page padding): a 640px frame is a 545px container and still shows the fold; the aside needs a frame of 735px or more. Write the chain in the spec; see `docs/lessons/2026-11-11-a-phone-width-criterion-needs-a-measured-chain.md` and #318 (the helper used to miss overflow a scrollbar hides; it compares with `clientWidth` now). The pinned classic scrollbar is 15px on the Mac but about 12px on the Linux CI runner (spec 069's first CI run, interior 281px vs 278px): `open_in_viewport_frame` sizes the frame from the measured scrollbar (`360 + innerWidth - clientWidth`), not 375. The zone table of the deck page's worst-case row (60-char name, 1,234 proxies) spills 7 to 10px past a 360px frame's 345 to 348px layout but not a true 360px viewport (spec 074, #318).
- **The one-screen scanner in a frame (spec 082, 2026-11-13):** the scanner opens fine in a narrow frame from the collection page, and Firefox's fake camera (the suite's media prefs) works inside it; `open_scanner_frame(width:, height:)` in `spec/support/scanner_helpers.rb` does this and waits for `.c-scan__feed`. The page never scrolls, so the frame draws no scrollbar and its width is the viewport's. A state that lasts only until the next await (the engine loading) is measured by calling the controller and reading the layout in the same `execute_script`, so nothing runs in between. Firefox's "Pop out this video" overlay can stick after a real pointer click near the video in screenshots; set `disablePictureInPicture` on the video for shots.
12 changes: 12 additions & 0 deletions .claude/memory/turbo-stream-string-is-html-safe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: turbo-stream-string-is-html-safe
description: turbo_stream.update/replace with a plain String marks it html_safe; escape any text holding a card or place name, or use a partial
metadata:
type: feedback
---

`turbo_stream.update(target, "some text")` (and `replace`, `append` …) passes a String template through `.html_safe` inside turbo-rails' `turbo_stream_action_tag`, so the string is inserted as HTML, not text. Spec 082's answers write sentences such as "Added Lightning Bolt MOM · 123, foil" and the list title "This sitting: 2 cards, in Blue binder" straight into stream targets; a location named `<b>bold</b>` would have rendered as markup.

**Why:** found while planning spec 082 (2026-11-13); the plan review confirmed it and `spec/requests/scanner/sitting_entries_spec.rb` pins the escaping with a place named `<b>bold</b>`.

**How to apply:** in a controller or concern, wrap any text that can contain a user's or the catalog's words with `ERB::Util.html_escape(...)` before giving it to a stream, or render a partial (`partial:` with locals escapes as a view does). Plan and code reviews check every `turbo_stream.*(target, <string>)` call. An empty string (`""`) is safe. Related: [[plan-reviews-run-the-code]].
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Ruby 4.0.7, Rails 8.1.4, SQLite in every environment (including production), Hot
- **Admin catalog and jobs (specs 015, 024, 055; ADRs 0014, 0015):** the refresh and art build lifecycles, stall and interrupt rules, rate-limit retries, "Retry now" for a waiting retry and the `/admin/catalog` and `/admin/jobs` pages are in [`docs/architecture/catalog-operations.md`](docs/architecture/catalog-operations.md). In specs, tag an example `:solid_queue` to queue through Solid Queue (no worker runs) and move jobs with `queue_job`, `claim_job`, `fail_job`, `finish_job`.
- **Footer and open catalog (specs 018, 023, 084, 087):** the footer's lines come from each source's `.content_notice`, the licence line is the core's last line, and core code never names a collectible (`spec/footer_core_spec.rb`, `spec/licences_core_spec.rb`). A new gem, pinned OCR package, importmap pin outside `app/javascript`, font or icon needs its entry in `config/third_party_licences.yml` (or, for a gem, a licence), or the suite fails. `Catalog.public?` is the one test of `COLLECTOR_PUBLIC_CATALOG`, never read from the environment in tests (tag an example `:public_catalog`), and specs set the build facts with `build_facts(...)`, never ENV; views use `collector?`, never the setting. Rules: [`catalog-core.md`](docs/architecture/catalog-core.md).
- **Active Storage variants (spec 014, ADR 0013)** use libvips through `ruby-vips`, which is bundled with `require: false` so the app boots without libvips (it then logs "requires the ruby-vips gem"). Development needs libvips (`vips` on Fedora, `libvips` on Debian/Ubuntu): `bin/setup` prints a hint when it's missing (`Collector::LibvipsCheck`), CI installs it, and `bin/image-smoke` fails an image whose variant transformer isn't vips. The only Active Storage upload is the import preview's stored file (`ImportUpload`, spec 028, ADR 0022), which meets ADR 0013's owed validation for itself (content type by content, size; no pixels, it is no image); pixel limits stay with the first image upload. Active Storage's routes are gated app-wide by `ActiveStorageGate` (`config/initializers/active_storage_gate.rb`): sign-in, ownership of original blobs only, and 404 for direct uploads and the disk service's upload PUT until a feature needs them; the first image feature extends ownership to variants and previews (ADR 0022).
- **Card scanner (specs 005, 007–011, 040, 042, 047):** `/scanner`, linked from the navigation ("Scan") and the collection page. How it reads a card, ranks candidates (with the optional art match), adds a copy, keeps the sitting with its condition and place, and records findings is in [`docs/architecture/scanner.md`](docs/architecture/scanner.md). Before you look: the OCR engine is fetched and checksum-verified into the ignored `vendor/ocr/v7.0.0/` by `bin/fetch-ocr-engine` (run by `bin/setup` and the Dockerfile), and scanner specs fail until it's installed; art matching (opt-in, `COLLECTOR_MTG_ART_MATCHING`) needs ImageMagick (`magick` or `convert`) for its build and specs; only scanner pages send a Content-Security-Policy (`ScannerPage` concern); the sitting's files name no collectible (`spec/scanner_sitting_core_spec.rb`); `script/scanner/detector_parity.rb` checks the detector (`app/javascript/scanner/detector.js`) against the spike's; measurement mode (`/scanner/measurement`, with the `scanner:` rake tasks and `script/scanner/` for the findings) is development-only (`config.x.scanner_measurement`; `COLLECTOR_SCANNER_MANIFEST`, `COLLECTOR_SCANNER_RUN_DIR`, `COLLECTOR_SCANNER_KEEP_FRAMES=1` keeps live frames) and writes outside the repo; the spikes are in `spikes/card_scanner/`. Phones need HTTPS: `bin/dev-certificate` makes a self-signed certificate outside the repo and prints the `bin/dev -b "ssl://…"` bind (alternative: a tunnel with `RAILS_DEVELOPMENT_HOSTS` + `COLLECTOR_HTTPS=true`).
- **Card scanner (specs 005, 007–011, 040, 042, 047, 082):** `/scanner`, a one-screen focused page (spec 082), linked from the navigation ("Scan") and the collection page. How it reads a card, ranks candidates (with the optional art match), adds a copy, keeps the sitting with its condition and place, and records findings is in [`docs/architecture/scanner.md`](docs/architecture/scanner.md). Before you look: the OCR engine is fetched and checksum-verified into the ignored `vendor/ocr/v7.0.0/` by `bin/fetch-ocr-engine` (run by `bin/setup` and the Dockerfile), and scanner specs fail until it's installed; art matching (opt-in, `COLLECTOR_MTG_ART_MATCHING`) needs ImageMagick (`magick` or `convert`) for its build and specs; only scanner pages send a Content-Security-Policy (`ScannerPage` concern); the sitting's files name no collectible (`spec/scanner_sitting_core_spec.rb`); `script/scanner/detector_parity.rb` checks the detector (`app/javascript/scanner/detector.js`) against the spike's; measurement mode (`/scanner/measurement`, with the `scanner:` rake tasks and `script/scanner/` for the findings) is development-only (`config.x.scanner_measurement`; `COLLECTOR_SCANNER_MANIFEST`, `COLLECTOR_SCANNER_RUN_DIR`, `COLLECTOR_SCANNER_KEEP_FRAMES=1` keeps live frames) and writes outside the repo; the spikes are in `spikes/card_scanner/`. Phones need HTTPS: `bin/dev-certificate` makes a self-signed certificate outside the repo and prints the `bin/dev -b "ssl://…"` bind (alternative: a tunnel with `RAILS_DEVELOPMENT_HOSTS` + `COLLECTOR_HTTPS=true`).
- **Export format and import (specs 017, 022, 025, 028, 032, 062, 068; ADRs 0020–0023, 0025, 0028):** the current rules are in [`docs/architecture/data-portability.md`](docs/architecture/data-portability.md); the format is defined by `docs/export-format/README.md`. Version 4 is current (`ExportFormat::Rules::VERSIONS` is `[1, 2, 3, 4]`); a new table with `account_id` or `user_id`, or a new column on `accounts`, `lots`, `users` or `locations`, fails the suite until the document's Coverage tables list it; a new field of collector-owned data raises the version.
- **Import preview and apply (specs 025, 028):** area doc above. The stored upload's retention and sweep (`ImportUpload`, `ImportUpload::SweepJob`, ADR 0022) and the 40 MB request body caps (Puma, `MAX_REQUEST_BODY` in the `Dockerfile`, `config/deploy.yml`) are in [`docs/architecture/release-and-deploy.md`](docs/architecture/release-and-deploy.md). `Location.upsert_all` and `CollectionImport::Apply#upsert_lots!` (a bound multi-row `INSERT .. ON CONFLICT`) skip validations on purpose (the reader already checked each row).
- **Collection and lots (specs 004, 006, 021, 026, 029, 032–034, 041, 059, 062, 064–068; ADRs 0019, 0025, 0028):** a lot's identity (printing, finish, condition, price paid and place, `lot_key`), the 9,999 cap, merging, Move copies, the grid and table, bulk mode and its Undo, number entry, locations and their names, and decks, their zones and the label "Deck › Zone" are in [`docs/architecture/collection-and-lots.md`](docs/architecture/collection-and-lots.md); how the export, preview and apply cover lots, locations and decks is in [`docs/architecture/data-portability.md`](docs/architecture/data-portability.md). Guards an agent trips first: `Location::Name` is the one definition of a name (`spec/models/location/name_parity_spec.rb`); five places build a `lot_key` and all must carry the place; `Account` declares `lots` before `locations` because the restrict foreign key needs lots deleted first (`spec/models/account_spec.rb`); a location has a kind, so a list says which one it means (every picker and the collection filter offer places and zones through `Location::Choices`, and a write names its location through `Location.find_placeable!`; the Locations page and the export stay places only; [`collection-and-lots.md`](docs/architecture/collection-and-lots.md)), and the deck files (the deck page's `Catalog::DeckCards`, `Catalog::DeckShape` and `Catalog::DeckChecks` seams among them) name no collectible, format or zone key but `considering` (`spec/deck_core_spec.rb`); number entry and the location files name no collectible type (`spec/number_entry_core_spec.rb`, `spec/import_apply_core_spec.rb`).
Expand Down
Loading
Loading