- Shipped
- July 22, 2026 at 5:31 PM UTC
- Author
- kamo
- Commit
- f31c03c
/leads/view/{id} died with "Cannot read properties of null (reading 'preferredName')". LeadDTO.toDTO() only builds primaryContact inside `if (lead.getName() != null)`, so a lead with no Name row arrives as `primaryContact: null` — but types/leads.ts declared it non-nullable, so the compiler never asked anyone to check. Same lie on firstName/lastName/email (nullable individually, and the redaction path nulls email/phones back out on a contact that does exist), phones and languages (each built under its own null check), and contacts, which the lead endpoints never populate at all. Telling the truth in the type turned tsc into the search: it found three sites, not one. The header, the summary rail's **************** — an optional chain stopping one hop short, which would have thrown the moment the header was fixed — and the headerName helper. The ADDRESS branch of the custom-form renderer had the same shape from a different direction: the stored value is untyped, so `JSON.parse(value || "{}")` returned null for a stored "null" and threw outright on legacy free text, then `.street` was read off it unguarded. Its DROPDOWN and RADIO siblings were already wrapped in try/catch; this one wasn't, and renderField() is called bare inside JSX with no error boundary, so either throw blanked the page. Normalizing at the two boundaries rather than at each call site: mapLeadResponse for fetches, and the websocket seed path. Only the seed path — the merge below it deliberately skips incoming nulls so a partial LEAD_UPDATED doesn't blank untouched fields, and normalizing first would have turned `primaryContact: null` into an empty object that overwrites a good contact, blanking the name on every status change. A missing name now renders an em dash.