Skip to content

040: Scan a Sitting into a Location - #172

Merged
plainprogrammer merged 14 commits into
mainfrom
040-sitting-into-location
Nov 6, 2026
Merged

plainprogrammer merged 14 commits into
mainfrom
040-sitting-into-location

Conversation

@plainprogrammer

@plainprogrammer plainprogrammer commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Implements spec 040 (v0.2.2) following its plan. A collector picks one of their locations for the scanner sitting, and every card added afterwards goes into the lot of that printing and finish in that place. The place can change mid-sitting (later adds only), survives a reload, and goes when the sitting ends. An account with no locations sees the scanner exactly as before. Nothing changes in the export.

What each phase did

Phase Commit What
1 31a9a0a scanner_sittings.location_id, nullable and indexed, REFERENCES locations ON DELETE SET NULL. Added by a raw ADD COLUMN outside a transaction (the lots migration's pattern), so no table is copied and no entry is lost. A migration spec proves down/up keeps every sitting and entry (AC-6.0).
2 e4bedff Scanner::Sitting.add! reads the place inside its transaction and adds through Lot.add!(location:). Sitting.place! opens, sets, clears or drops the sitting. #pointless? implements P1's narrower rule. kept_entries preloads each lot and its location.
3 35b0339 PATCH /scanner/sitting with sitting[location_id] answers with a Turbo Stream that redraws the panel. The location is looked up through Current.account.locations: a bad value is a 422 with decision 7's alert, a missing key a 400. The picker appears in the panel, alone when no sitting is open, and under the Done summary. The add notice names the entry's lot's place.
4 43d462c The panel: "This sitting: N cards, in Blue binder", "in " on each entry, and the three empty-sitting copies. Scanner.md is updated.
5 7a6256a Headless Firefox flows: choose, scan, change, reload, clear, Done, keyboard only, and a 360px frame with a long name. The 360px example found two overflows, fixed here (see below).
6 d259770 The collection-and-lots area doc, the roadmap (21a built, run before the spikes by ruling) and the specs index (supersessions of 009 and 032, both directions).
Evidence e286815, f392321 Screenshots and GIF; verification.md with per-AC evidence and the implementation review.
Review fixes 27862c7 See the review section.
Wrap 1c1ab63, 9a4ded9, 2649e4b, 7aa45a1 A lesson (long names in narrow checks), the distillation into collection and lots and data portability, the frozen line, and the distillation record.

Rulings and deviations

  • One commit per phase, tests and implementation together, per the dispatch brief, instead of the plan's separate test: and feat: commits.
  • No subagents for implementation, per the brief, even though sdd-execute says to use them. I implemented each phase directly with TDD (red, then green). Only the Mode B review ran as an agent.
  • P2 (the picker's CSS): the stacked Form with no new component. Two existing rules changed after the 360px check failed:
    • .c-scanner__sitting .c-section__title now wraps, like .c-panel's title already does.
    • .c-field select gets min-width:0; max-width:100%, so a long option can't widen it past its form. This is app-wide: it also covers the lot form's location select. Form.md says so.
  • Status line: when clearing the place deletes the sitting, the status line is emptied, as the plan says (no message).
  • Measurement mode drops an empty sitting the same way the scanner does (from the review).
  • Commit trailers: 31a9a0a and e4bedff say Co-Authored-By: Claude Sonnet 5.5 by mistake. This session ran on Opus 5.5, and every later commit says so. Pushed commits were not rewritten.

Tests

  • Whole suite at 7a6256a: 2467 examples, 0 failures, 1 pending (the art agreement example, which needs the spike's cache).
  • After the review fixes: 2470 examples (a dry run counts them), run in full by the pre-push bin/ci at 7aa45a1; the scanner model, request and system specs re-run after the fixes: 152 examples, 0 failures.
  • New and extended: the migration spec (5 examples), spec/models/scanner/sitting_spec.rb (+16), spec/requests/scanner/sittings_spec.rb (+22), sitting_entries_spec.rb (+5), measurements_spec.rb (+1), apply_spec.rb (2 examples extended), spec/system/scanner_sitting_spec.rb (+5).
  • Local bin/ci: passed in full as the pre-push hook at 7aa45a1 (setup, migration guard, RuboCop, Brakeman, bundler-audit, importmap audit, RSpec). One earlier pre-push run failed Upgrade::Harness removes its temporary directory, because another worktree's suite was running at the same time (Flaky: Upgrade::Harness "removes its temporary directory" races other suites through the shared tmpdir #178). It passed on the retry.

Implementation review

SPEC-ALIGNED (sdd-review Mode B, read-only, Opus). The coverage matrix and how each finding was handled are in verification.md.

  • Major: verification.md was missing. It has been written.
  • Fixed, tests first:
    • The scanner checked whether a sitting was empty and deleted it in two separate steps, so an add from another tab in between could lose its entry. drop_if_pointless! now reloads, checks and deletes in one transaction.
    • Measurement mode now drops empty sittings too.
    • A guard proves another account's locations never appear in the select.
    • Form.md documents the select's width.
  • Kept:
    • Focus after "Set" drops to the page, as it does after spec 009's Undo.
    • The status line stays empty when clearing deletes the sitting, because the plan says no message there.
    • place! writes the row twice (insert, then update) when it opens a sitting. This is harmless.

Screenshots

Headless Firefox against this worktree's dev server. Its database was empty, so a scratch script seeded it: one collector, three printings, two locations and an applied refresh run. Tiles show placeholders, not card art.

The add uses a picked photo: the specs' synthetic card, because there is no camera headless. The 390px shots come from a 390px iframe on /collection, because the scanner sends frame-src 'none'. Headless Firefox reports a dark colour scheme, so the walkthrough is dark.

Walkthrough: choose Blue binder, pick a photo, add the Foil copy, and the lot shows Blue binder in the collection.

Walkthrough

390px, light 390px, dark
Panel at 390px, light Panel at 390px, dark
Desktop, light Desktop, dark
Panel, desktop, light Panel, desktop, dark

The added lot in Blue binder

Check by hand

  • On a phone with a real camera, choose a place, scan a few cards, and check that the camera keeps running across "Set".
  • Remove a location that holds no cards while a sitting names it, then scan a card. The notice should name no place.
  • The panel's spacing: the stacked picker sits between the heading and the list.

Closes #56

🤖 Generated with Claude Code

plainprogrammer and others added 6 commits November 5, 2026 23:30
Spec 040 Phase 1 (AC-6.0, NFR Migration): scanner_sittings.location_id,
nullable, indexed, REFERENCES locations ON DELETE SET NULL, added with a
raw ADD COLUMN outside a transaction (the lots migration's pattern), so
no table is copied and no entry is lost. Down drops the column whatever
it holds: the place is working state.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec 040 Phase 2 (AC-1.3-1.6, AC-1.8, AC-4.3, FR-1, FR-2): the sitting
belongs to an optional location, Sitting.add! reads it inside its
transaction and adds through Lot.add!(location:), Sitting.place! opens,
sets, clears or drops the account's sitting, #pointless? marks a sitting
with no entries at all and no place (plan P1 as ruled), and kept_entries
preloads each entry's lot and its location.

Ruling: one commit per phase (test and implementation together) per the
dispatch brief, instead of the plan's separate test/feat commits.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec 040 Phase 3 (Stories 1-5, FR-1): PATCH /scanner/sitting with
sitting[location_id] opens, sets, clears or drops the account's sitting
through Sitting.place!, answering a Turbo Stream that redraws the panel.
The location is found through Current.account.locations; another
account's, a removed one or a non-number is a 422 with the decision 7
alert, a missing key a 400. The picker partial renders in the panel,
alone with no sitting open, and under the Done summary, for accounts
with locations only. The add notice names the entry's lot's place, and
the scanner drops a sitting with no entries and no place before render
(plan P1 as ruled). Import apply specs now carry a placed sitting
(AC-3.2's import case, AC-5.3).

Ruling: a status message is cleared (no text) when clearing the place
deletes the sitting, as the plan says no message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 040 Phase 4 (decisions 3, 4; AC-1.2, AC-2.3, Story 7): the heading
reads "This sitting: N cards, in Blue binder" while a place is set, each
entry names its lot's place as it is now (none for a changed entry), and
an entry-less sitting says "Cards you scan now go into Blue binder." or,
with no place, "Nothing added yet."; spec 009's all-undone copy stays.
The Scanner component doc describes the control and the copy.

Ruling (plan P2): the picker is the stacked Form (c-form, c-field,
c-form__actions) with no new CSS; the 360px system check in Phase 5 and
the screenshots confirm it fits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 040 Phase 5 (Stories 1, 2, 5, 7; AC-1.2, AC-1.7, AC-2.1): headless
Firefox opens a sitting in a place without reloading the page or its
camera, scans into it, changes the place mid-sitting and scans again,
keeps the place on a reload, clears it with cards kept, ends with the
control on No location, chooses a place by keyboard alone, and fits a
360px frame with a long location name.

The 360px example found two overflows, fixed here: the sitting's heading
kept c-section__title's nowrap (it now wraps, as .c-panel's title does),
and .c-field select grew to its longest option (min-width:0 and
max-width:100%, which also covers the lot form's location select).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 040 Phase 6: collection-and-lots says the scanner adds into its
sitting's place and that removing a location clears a sitting's place
(decisions 2, 4, 7; AC-1.3, AC-3.2), drops the #56 open item and cites
spec 040; the roadmap marks item 21a built and records that it ran
before the spikes by ruling (decision 1); the index records 040's
supersessions on the 009 and 032 rows. Specs 009 and 032 are not edited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
plainprogrammer and others added 7 commits November 6, 2026 00:40
Headless Firefox against the worktree's dev server, whose empty database
was seeded by a scratch script (one collector, three printings, two
locations, one applied refresh run; tiles show placeholders, not art):
choosing Blue binder for the sitting, confirming a picked photo (the
specs' synthetic card; no camera headless) and the lot in the
collection; the panel at 390px from a 390px iframe on /collection (the
scanner sends frame-src 'none') and at desktop width, light and dark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er pages

From spec 040's implementation review (minor findings): the scanner
dropped a sitting with no entries and no place by checking and deleting
in separate statements, so an add from another tab in between could
lose its entry; Scanner::Sitting#drop_if_pointless! now reloads, checks
and deletes in one write transaction. current_sitting moves into the
ScannerSitting concern so measurement mode drops it too. A guard proves
another account's locations stay out of the select (FR-1), and Form.md
records that a select never widens past its form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per-AC evidence, the requirement and document checks, the screenshots,
and the Mode B review (SPEC-ALIGNED) with how each finding was handled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 040 lesson: put the longest user-supplied name in every
narrow-width check (a nowrap section title and a flex select overflowed
at 360px), and read a destroyed record's associations only after
checking it persisted. 0 memories written, 1 lesson written.

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

Collection and lots already carried spec 040's statements (Phase 6); it
gains its distillation log row. Data portability's working-state
invariant now names the sitting's place (decision 5, AC-5.3) and an
apply that removes only a location (AC-3.2), with spec 040 in Sources
and the log. The index marks 040 frozen in part by both docs, plan
retirement owed. The scanner has no area doc, so the sitting's own
rules stay in the spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… portability area docs (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: #182 (keep focus and announce after the sitting panel redraws), #178 (harness tmpdir race). For the maintainer: on a real phone, check the camera keeps running across Set. Note the app-wide CSS change: .c-field select gains min-width:0; max-width:100% so a long location name can't widen a form.

@plainprogrammer
plainprogrammer marked this pull request as ready for review November 6, 2026 05:58
@plainprogrammer plainprogrammer self-assigned this Nov 6, 2026
Brings in specs 037 (filter the collection by location) and 039 (a
missing art index file isn't ready). Conflicts resolved by keeping both
sides: db/schema.rb regenerated by migrating (main's bulk_selections
column plus this branch's scanner_sittings.location_id, its index and
foreign key); collection-and-lots.md keeps spec 037's and spec 040's
Sources lines, both distillation log rows, #47 and #56 closed; the
specs index takes main's 039 row and this branch's 040 row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plainprogrammer
plainprogrammer merged commit 2e2fdf1 into main Nov 6, 2026
9 checks passed
@plainprogrammer
plainprogrammer deleted the 040-sitting-into-location branch November 6, 2026 06:18
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.

Scan a sitting into a location

1 participant