PR 1900 keeps CRM numeric controls as editable strings until their form boundary, so deletion no longer paints zero back into the field. Required, optional, integer, decimal, minimum and maximum contracts remain field-specific. The final candidate also rejects a blank visit-pack credit count, fractional guest passes, fractional stock, blank required sort values and newly entered non-HTTP(S) product image URLs while preserving unchanged legacy image values.
fix/1880-numeric-input-editing at code head 956789388190002261d0bdb40c90f03fd85e65d6; this evidence source is the following report update
Generated
Reviewer brief
Outcome and scope
Blank editing, replacement, decimal input and explicit validation are observable
The original browser captures reproduce the duration snapback and candidate blank/error states. The rebased candidate preserves those fixes and adds submit-time validation for visit-pack credits, guest passes, product stock, required sort order and product image URLs. Focused component suites total 38 passing tests and CRM typecheck passes.
Evidence boundary
The duration and stock screenshots were captured from exact pre-rebase revisions 6049e37fc and 707767a4f. The final rebase did not change those pictured controls, but the final validation corrections were added afterward and are evidenced by current-head component tests and source excerpts rather than recaptured screenshots. No production data was used.
Claim map
Evidence coverage
Each material changed behavior or state maps to direct evidence or an explicit gap.
Behavior or claim
Evidence
Status
Clearing a required numeric field preserves an empty editing value
Current-head ClassFormDialog interaction test asserts input.value is empty; matched screenshots show the old zero value versus candidate validation, whose visible 60 is placeholder text
Covered
Required blanks and invalid optional values use field-specific React validation
Duration and stock captures plus current-head requestSubmit tests
Covered
Visit-pack credits remain positive decimals and guest passes remain whole numbers from 0 through 100
New product image URLs accept only HTTP(S), while unchanged legacy values remain editable
ProductFormDialog current-head tests cover a malformed value, javascript: and an unchanged legacy path; the shared isHttpUrl guard rejects other non-HTTP(S) schemes; issue #1915 tracks server-side enforcement
Covered
Class, hook and membership numeric boundaries preserve typed editing until submit
Current-head Class 11, Membership 14, Product 6 and Hook 7 test suites
Covered
Observed change
Review evidence
Evidence is grouped by the reviewer question each item addresses.
Sessions, booking fields
Screenshot
Deleting Duration no longer snaps to zero
The same local Serenity session form at 1280 by 900 CSS pixels. The merge-base eagerly coerces the cleared string to zero. In the candidate the DOM value remains empty, the browser displays the field placeholder 60, and submit renders the form-owned required error.
Merge-base after selecting and deleting Duration: React immediately paints 0 back into the number control.BeforeCandidate after deleting Duration and submitting: the DOM value is blank; the visible 60 is placeholder text, and Duration is required appears below.After
Products
Screenshot
Invalid optional stock reaches the form-owned error
The Product form carries noValidate, so submitting negative stock reaches React validation and keeps the dialog open.
Stock remains -1, is marked invalid and displays Stock cannot be negative instead of being blocked silently by the browser.After
Validation contract
Table
Observed and automated form-boundary cases
Each value class is retained as editable text, then validated and converted once at submit.
Case
Expected contract
Evidence
Required blank numeric
Keep blank and refuse submit with a field message
Duration capture; Class, Membership and Product tests
Optional blank numeric
Submit null rather than zero
Product interaction test
Explicit zero
Remain legitimate where the field allows zero
Stock capture and Product tests
Positive decimal credits
Preserve up to two decimals and reject blank or non-positive
Membership tests and source guard
Whole-number fields
Reject fractions instead of truncating
Class, Membership and Product tests
Guest passes
Accept only whole numbers from 0 through 100
Membership tests and source guard
Product image URL
Accept HTTP(S), refuse unsafe new schemes, preserve an unchanged legacy value
Product URL validation tests
Membership plan validation
Code
Credits and guest passes keep distinct numeric contracts
Visit packs require a positive decimal credit count with at most two decimal places. Recurring-plan guest passes remain optional but must be a whole number from 0 through 100.
14 passed, 0 failed, 51 assertions, including blank credit count and fractional guest passes
Pass
Product form
6 passed, 0 failed, 29 assertions, including stock/sort coercion and image URL safety
Pass
Hook form
7 passed, 0 failed, 26 assertions
Pass
CRM typecheck
bun run --filter @sauna-crm/crm typecheck exited successfully on the rebased candidate
Warn
Final-head screenshot matrix
Existing captures cover unchanged duration and stock states at the cited pre-rebase revisions; final validation additions are covered by current DOM interaction tests, not new screenshots
Limits
Known issues, risks, and gaps
Current-head tests cover all changed form contracts; image URL enforcement remains client-side until issue #1915 is resolved
Product image URL enforcement is not yet an API invariant The CRM refuses unsafe new values and rendering uses the shared safe-URL helper, but another client can still submit one. Issue #1915 tracks server-side enforcement.
Some visual evidence predates the final rebase The pictured duration and stock behavior is unchanged, and current-head interaction tests defend the final DOM contract. No screenshot is presented as proof of the later image URL, credit-count or guest-pass corrections.