Skip to content

037: Filter the Collection by Location - #174

Merged
plainprogrammer merged 10 commits into
mainfrom
037-filter-collection-by-location
Nov 6, 2026
Merged

plainprogrammer merged 10 commits into
mainfrom
037-filter-collection-by-location

Conversation

@plainprogrammer

@plainprogrammer plainprogrammer commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Implements spec 037 (0.2.2) per its plan. The collection's filter bar gets a Location select: All locations, No location, or one of the collector's locations. The grid, the table, paging, sorting and bulk mode all honour it, combined with the name search (AND).

Walkthrough: filter the grid by Blue binder, then No location, switch to the table, Edit many, Select all, Remove, Undo

What each phase did

  • Phase 1, models (ff995c1)
    • CollectionFilter::Place turns location into its canonical form: "", "none", or an id of at most 18 digits. Anything else counts as all. It is then applied as a plain lots.location_id predicate.
    • CollectionFilter reads the account's locations once (places?, the options, the chosen name) and checks the URL's value against them for display. Under a location filter the grid skips its place line.
    • bulk_selections.location_filter is a new string column, default "", null: false, reversible. BulkSelection#context?, #restart!, #lots and #record! use it, never re-resolved, so a removed or another account's location selects nothing (decision 7).
  • Phase 2, controllers and helpers (1c57170)
    • The page, collection_listing_path (which leaves out all), the sort headers, Edit many and the bulk form all carry location.
    • Bulk submissions use the rendered rendered_location for their context, pagers, sorts, actions and Done. Only Filter applies the select's value, and it keeps the page only when neither filter changed.
    • BulkSelected's return path carries the stored location; Undo needed no change.
  • Phase 3, views (2f4c2bd)
    • The FilterSelect sits in the filter bar with a Filter button, shown only while the account has a location.
    • In bulk mode it joins the bulk form, and the existing default go=filter button becomes visible beside it; it is still the first submit, so Enter still applies the filters.
    • The view-switch and Edit-many forms and both pagers carry location.
    • The empty sentences name the place: "No cards in your collection are in “Blue binder”.", "…are without a location.", and the forms with a query.
  • Phase 4, measurement and documents (0690b63)
    • A :timing request spec: 5,000 lots over 300 locations, plus two other accounts' 5,000 lots each. Every page is under 500 ms (largest: the unfiltered table at 163 ms; with a filter, 21–106 ms), so the reserve [account_id, location_id] index is not added.
    • Updated: verification.md, the collection-and-lots area doc, the roadmap, the specs index and CLAUDE.md.
  • Review follow-ups (afe0ce8): tests for findings 1–3 (below).
  • Wrap-up
    • Screenshots and review record (96c3fbe).
    • Session learnings (46f7ae5): screenshot memory, one lesson.
    • Distillation (1acece1, 5b47f03, 8628b13): the index row is Frozen by collection and lots, plan retirement owed.

Rulings and deviations (each also in its commit body)

  • No per-phase subagents. I implemented each phase myself, test first, because the Orca brief forbids helper agents other than the review.
  • Empty sentence as a partial. collections/_no_match replaces the plan's no_match_message helper. A helper's String is escaped whole, which turned spec 004's literal quotes into " and broke three existing body assertions. The partial keeps them raw without html_safe.
  • Migration rollback. The down uses SQLite's native ALTER TABLE … DROP COLUMN, so the table isn't copied and its marks aren't cascaded.
  • No CSS. FilterSelect's existing rules suffice; the 390px system spec checks both modes.
  • AC-5.8 refusal shows All locations. The removed location no longer resolves (decision 7).
  • System spec written after the views. The request markup specs were the red step for Phase 3.
  • Measurement as a timing spec. It runs with SCANNER_TIMING=1 and never gates.
  • Commit trailers. ff995c1 and 1c57170 say Co-Authored-By: Claude Sonnet 5.5 by mistake. The work ran on Opus 5.5; later commits say so, and the pushed commits were not rewritten.

Implementation review: SPEC-ALIGNED

An independent read-only sdd-review Mode B on Opus found nothing blocking or major. It reported five minor findings:

  1. A selection's ticks shown under a location had no direct test. Fixed (selection_state_spec).
  2. The confirm page's Cancel/Back links, and the confirm page for a removed location, weren't asserted. Fixed.
  3. The filtered table's pager count was covered only indirectly. Fixed.
  4. verification.md sections were missing. Fixed.
  5. A local Gemfile.lock platform change. Kept out of every commit.

The review also traced tenancy, the fail-closed paths and every caller; details are in verification.md.

Tests

  • New: place_spec (4), collection_location_filter_spec requests (21), collection_location_select_spec markup (14), system spec (6).
  • Extended: grid (+8), table (+3), bulk selection (+5), bulk removal (+1), selection state (+1).
  • Not in the suite: one timing example, excluded unless SCANNER_TIMING=1.
  • Suite: 2,483 examples, 0 failures, 1 pending. Local bin/ci is green, also in the pre-push hook on 8628b13.
  • Flake: one earlier pre-push run failed on the known cross-worktree tmpdir race in Upgrade::Harness (Flaky: Upgrade::Harness "removes its temporary directory" races other suites through the shared tmpdir #178), unrelated to this change. It passed on re-run.

Screenshots

Taken with headless Firefox against a dev database seeded with placeholder printings (no card art): one collector, three locations, 14 lots.

Desktop, light Desktop, dark
Grid under Blue binder, light Grid under Blue binder, dark
Table under Blue binder, light Table under Blue binder, dark
No location, light Bulk mode under Blue binder, light
After Remove under Blue binder After Undo, still under Blue binder
Phone 390px, light Phone 390px, dark
Phone grid, light Phone grid, dark
Phone bulk, light Phone bulk, dark

Worth checking by hand

  • A real phone: the native picker for the select, and touch targets (coarse pointer gives 40px). Headless Firefox can't test either.
  • Bulk filter bar layout: a visible Filter button now sits beside the select in bulk mode for accounts with a location. Accounts without one keep the old screen-reader-only button.
  • Measurements: taken in the test environment on the dev machine, as spec 033's were.

Closes #47

🤖 Generated with Claude Code

plainprogrammer and others added 10 commits November 5, 2026 23:28
… selection

Spec 037 plan Phase 1 (model layer). CollectionFilter::Place parses the
`location` value into its canonical string ("", "none" or an id of at
most 18 digits) and applies it as a plain predicate on lots.location_id.
CollectionFilter reads the account's locations once (places?, the
select's options and the chosen name) and resolves the URL's value
against them for display; the grid drops its place line under a location
filter. bulk_selections gains location_filter (default "", null false);
BulkSelection compares and restarts with it and applies it unresolved,
so a removed or foreign location selects nothing (decision 7).
BulkRemoval keeps the location when it restarts the selection.

Ruling: the down migration drops the column with SQLite's native
ALTER TABLE DROP COLUMN rather than remove_column, so the table isn't
copied and its marks aren't cascaded. Cost if wrong: a rollback on a
SQLite older than 3.35, which Rails 8.1 doesn't support anyway.
Ruling: implemented by this session itself, not per-phase subagents,
as the Orca brief forbids helper agents except the review.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ge, bulk mode and their redirects

Spec 037 plan Phase 2 (controllers and helpers). The page reads
`location` into the grid or table; collection_listing_path carries it
and leaves it out for all; the sort headers carry the table's resolved
location. Edit many stores the parsed value; a bulk submission uses
`rendered_location` (canonical, never re-resolved) for its context,
pagers, sorts, actions and Done, and Filter applies the select's value,
keeping the page only when neither filter changed. BulkSelected's
return path carries the stored location; the Undo controller needed no
change (it re-reads its return URL).

Request specs cover AC-1.2–1.4, 2.1, 3.1, 4.2, 5.1–5.3, 5.5, 5.6, 5.8,
6.1, 6.2 (two accounts) at the controller level.

Ruling: AC-5.8's refusal page re-renders under the URL's location,
which no longer resolves, so it shows all locations (decision 7); the
count line has no "of". Asserted on the count line here; the select's
markup is Phase 3.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec 037 plan Phase 3. The filter bar shows the design system's
FilterSelect ("All locations", "No location", then the account's
locations in Location.ordered order) with a Filter button, only while
the account has a location. In bulk mode the select joins the bulk
form and the existing default `go=filter` button is made visible beside
it (still the first submit in tree order); the form carries
`rendered_location`. Pagers, the bulk pager, the view switch and Edit
many carry the location; the empty sentence names the location or
"without a location".

Ruling: the empty sentence is a partial (collections/_no_match), not the
plan's `no_match_message` helper: a helper string is escaped whole, so
its literal quotes came out as &quot; and broke spec 004's existing
body assertions; a partial keeps them raw with no html_safe. Cost if
wrong: none in the browser, the text is the same.
Ruling: no CSS was needed; the select and button sit in
.c-filterbar__filters as FilterSelect lays them out (checked at 390px
by the system spec).
Ruling: the system spec was written after the views (the request
markup specs were the red step for this phase).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 037 plan Phase 4. Measures the collection page under a location
filter (5,000 lots over 300 locations, two other accounts' lots in the
table): every page under 500 ms (largest: the unfiltered table,
163 ms), so the reserve [account_id, location_id] index is not added.
verification.md records per-AC evidence and the statement counts; the
implementation review and manual checks follow. The collection-and-lots
area doc gains the location filter, tiles under it and the selection's
location (each with its source) and drops #47 from "Not yet"; the
roadmap, the specs index and CLAUDE.md's locations paragraph say it is
built.

Ruling: the measurement is a `:timing` request spec (run with
SCANNER_TIMING=1, never gating), per the brief's "measurements as
timing specs".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ble pager under a location

Findings 1–3 of spec 037's implementation review (Opus, read-only,
SPEC-ALIGNED, minor): SelectionState shows a selection's ticks under its
location and nothing under another (AC-5.3); the removal confirm page's
Cancel and Back carry the location and the filtered table pages its
lots (AC-5.6, AC-1.4); the confirm page for a selection whose location
was removed sends the collector back with "Select at least one item."
(AC-5.5). The code already did each; these pin it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the Mode B review (SPEC-ALIGNED, five minor findings: three
fixed by afe0ce8's tests, this file's sections, the local Gemfile.lock
kept out) and the manual checks from the PR evidence: desktop and 390px
shots in light and dark, and a walkthrough GIF of the filter in grid
and table view, then bulk mode's Select all, Remove and Undo under it.
Taken against a dev database seeded with placeholder printings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session-wrap for spec 037: the screenshot memory gains that headless
Firefox prefers dark and Turbo keeps <html>'s data-theme, so light shots
must set it too; a lesson records that a helper returning a sentence
escapes its literal quotes, so copy that mixes them with user text goes
in a partial.

Ruling: written without a maintainer prompt, as the Orca brief asks the
worker to run session-wrap and commit what it writes; limited to two
candidates with direct evidence in this session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The area doc's rules for the location filter came in with Phase 4
(0690b63); this adds its Distillation log row and lists spec 037 under
the area in docs/architecture/README.md. The index row is Frozen, by
collection and lots, with its plan's retirement owed; its supersessions
were already recorded in both directions (specs 004, 006, 033). The
FilterSelect note is the design system's (its component doc).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (spec 027 D4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plainprogrammer

Copy link
Copy Markdown
Owner Author

Follow-ups filed by the coordinator: #181 (shard 1's Net::ReadTimeout at sign-in, re-run green), #178 (harness tmpdir race). For the maintainer: check the location select on a real phone (native picker, touch target).

@plainprogrammer
plainprogrammer marked this pull request as ready for review November 6, 2026 05:58
@plainprogrammer plainprogrammer self-assigned this Nov 6, 2026
@plainprogrammer
plainprogrammer merged commit 4d2a3fc into main Nov 6, 2026
15 of 17 checks passed
@plainprogrammer
plainprogrammer deleted the 037-filter-collection-by-location branch November 6, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Filter the collection by location

1 participant