Build image / build-and-push (push) Successful in 17s
The correctness finding is a real bug and the interesting one: arming a plant from the Seed Tray never attributed a seed lot, not even in the unambiguous single-lot case. The tray is the QUICKEST way to place a plant, so the fastest path was the one that silently lost which packet the seed came from — and it would have looked fine, because attribution is invisible until you go looking at a remaining count that's wrong. armPlant now does the same single-lot auto-attribution the picker does. Several lots stays unattributed rather than guessing; the picker is where that choice belongs. A single exhausted lot was also indistinguishable from having no lot, because the "available" filter dropped it before the count was taken. That silently dropped attribution with no signal. attributableLots now offers exhausted lots too, just not first: the count records what was written down, not a fact about the packet, so refusing to attribute a planting because the number says zero makes a wrong number harder to correct rather than easier. It's also now one rule in one place instead of the same filter written twice. The seed lot form had no 409 handling, alone among the version-guarded forms here. A concurrent edit made you retype everything; it now rebases onto the current row and says so, like the garden and plant forms do. Deduplication: the outbound-link anchor was duplicated verbatim between the plant card and the lot rows, including the security-relevant rel attribute — now one SourceLink that re-checks the URL and renders nothing when it isn't safe, so callers can't forget to guard. remainingLabel had its own copy of the unit-agreement rule that summarizeLots already owns; it now calls it. formatQuantity moved out of a component file into lib/seedLots.ts with the other formatters. STATE_CLASS.over and .low were identical. "Over-planted" is a discrepancy to look at, not a shortage to act on, so it now reads as its own warning. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ