/practicas/nueva validates only at submit - 260 blocked submits, 23 of 29 active users, one user 37 in a single day
MEASURED 2026-08-22, prod appEvents 30d: 260 toast error-shown rows, 23 distinct actorUserId (only 2 rows have a NULL actor), against 823 prácticas created by 27 creators. That is ~1 blocked submit per 3.2 saved prácticas, and 23 of the 29 users active in the window hit at least one. Worst single user: 37 in ONE day (d7ecba79); next 32 over 3 days; a third 30 over 7 days. Top codes: pieza-o-region 32, fecha-requerida 28, dob-requerida 25, provincia 24, sexo 22, cirugia 19, anestesia 19, modulo 17, sede 13. MECHANISM (src/components/practica-form.tsx:618-716): every required-field check lives in the submit handler; the checks array is evaluated only on submit, errs is collected, all fields are flagged at once and the FIRST is scrolled to. Nothing validates on blur or change, so a user filling a long clinical form discovers every omission at the end, after scrolling past it. The all-at-once flag is good; the timing is the defect. Proposed: run the same checks array on field blur and render the field error inline at that moment, keeping the submit pass unchanged as the authority (same shape as the client-side validateReason in the VENUS-372 AddParticipantBox - ONE rule, called from two places, no second copy to drift). Note pieza-o-region cannot be fully solved this way: its requirement is per-categoría (requierePieza/requiereRegion read server-side in crearPractica), so the form must surface WHICH is required for the chosen cirugía at selection time, not just flag its absence at the end.
Activity
-
PREMISE CORRECTION, measured against prod BEFORE building: the "say WHICH of pieza/región the chosen cirugía needs" leg of this row is VOID today. cirugiasCategorias = 0 rows, cirugiasSubcategorias = 0 rows, cirugias = 171 rows of which 171 have subcategoriaId NULL. So the server JOIN in practica-actions.ts:440-479 can never match, catRow.rowCount is always 0 and it always takes the legacy OR branch; client-side categoriaOpts is always empty, categoriaRow is always undefined, and requierePieza/requiereRegion both collapse to !isATM - also OR. Client and server agree today, and the 32 pieza-o-region hits are users submitting with NEITHER a pieza nor a región, not a rule mismatch. The generic inline mechanism this row specifies is therefore the whole fix for that code too. The client/server divergence is real but DORMANT; filed separately rather than fixed inside the extraction commit.
-
SHIPPED f9185e7 (venus 1.17.10). src/lib/practica-checks.ts is now the single rule: buildPracticaChecks(snapshot) returns the {field,code,bad}[] array lifted verbatim out of onSubmit, collectFieldErrors(checks) is the first-bad-per-field fold returning CODES (not messages, so onSubmit can log them and render resolves the copy), visibleErrors(all, touched, submitAttempted) decides what the user may see. Errors are DERIVED every render from one snapshot object that onSubmit also reads - the only state is touched:Set<string> + submitAttempted. Blur-driven setFieldErrors was rejected as the wrong shape: piezaRegion and participantes have no blur event of their own and would stay stuck red after the user fixed them. Not memoised on purpose: ~20 booleans over values already in hand vs a ~25-entry deps array whose staleness is a validation bug no passing test catches. THE EMIT DID NOT MOVE - logGuardrail stays in onSubmit alone, because a blur fires per field-exit and logging from the shared rule would inflate the counter this row was measured on by orders of magnitude; practica-checks-no-emit.test.ts pins that with a vi.mock of log-guardrail whose factory throws. Touch wiring keys on the ANCHOR field, not the input: historiaClinica blur touches dni (where dni-o-hc anchors), the No-disponible checkbox touches pacienteDob, dni composes with the existing lookupDni. DateDmyField gained onFieldBlur rather than a raw onBlur pass-through - three focusable segments, so a plain blur fires on every day-month-year tab; the wrapper tests relatedTarget. FlashField.invalid untouched (VENUS-279 emptiness cue, a different contract). FINDING worth keeping: the first-bad-per-field ORDERING could not be falsified through any snapshot - the only two fields with two checks each have mutually exclusive predicates, so a last-wins swap left every snapshot-driven test green. Pinned instead against a synthetic PracticaCheck[]. 8 falsification controls all verified to bite. 847 tests pass, tsc 0, lint 0 errors.
-
DEPLOY CONFIRMED: /api/health at 2026-09-11T14:22:37Z returns version 1.17.12, deploymentId dpl_6Xr9Gx2KXsiANNV5FJvG8AsZt41y, dbOk true. 1.17.12 is past the 1.17.10 bump that carried f9185e7, so the inline-validation build is live in production.