diff --git a/apps/pwa/src/components/EventForm.tsx b/apps/pwa/src/components/EventForm.tsx index 10a99ab..a8980cf 100644 --- a/apps/pwa/src/components/EventForm.tsx +++ b/apps/pwa/src/components/EventForm.tsx @@ -350,7 +350,13 @@ export function EventForm() { // D-06: validate recurrence bound inputs (T-06-06-input: client-side UX gate) if (recurrence !== 'none') { - if (recurrenceBound === 'count' && recurrenceCount < 1) { + if ( + recurrenceBound === 'count' && + (!Number.isInteger(recurrenceCount) || recurrenceCount < 1) + ) { + // WR-03: NaN < 1 is false, so a NaN count (from inputs like '' / '-' / 'e') + // previously bypassed this guard AND the payload spread, yielding an unbounded + // series. Require a finite integer ≥ 1 explicitly. newErrors.recurrenceBound = 'Must be at least 1 occurrence' } else if (recurrenceBound === 'until') { // WR-02: a blank end date with bound='until' must be a validation error. @@ -939,7 +945,13 @@ export function EventForm() { min={1} placeholder="e.g. 10" value={recurrenceCount} - onChange={(e) => setRecurrenceCount(Number(e.target.value))} + onChange={(e) => { + // WR-03: parseInt + Number.isFinite guard. Number('') === 0 and + // Number('-'/'e') === NaN both previously slipped through; coerce any + // non-finite intermediate to 0 so the validate() guard catches it. + const n = parseInt(e.target.value, 10) + setRecurrenceCount(Number.isFinite(n) ? n : 0) + }} style={{ ...inputStyle, ...(errors.recurrenceBound ? { borderColor: 'var(--color-destructive)' } : {}),