Track L — SMS notifications (#123)#136
Merged
Merged
Conversation
The DB stores dates as ISO datetimes (e.g. '2026-05-29T09:00:00'). This value was loaded verbatim into the form, causing two problems: 1. <input type='date'> could not parse it (showed empty placeholder) 2. sanitizeSettingsPatch bypassed the date→ISO conversion, so the API received a datetime without a timezone suffix, failing z.string().datetime() Fix: strip the time component on load so the date picker gets 'YYYY-MM-DD', which sanitizeSettingsPatch then correctly converts to a Z-suffixed ISO string. Applied to closingDate too for consistency.
api.team.members returns {data:{members:[...]}} — the loader was reading
body.data (an object) and calling .filter() on it, causing a TypeError crash.
Align with the BFF sheet route which correctly reads data?.members.
…ings BFF pageSize: "200" fails Zod refine (valid: 12/25/50/100); the .catch(() => null) silently swallowed the 400 so the template dropdown was always empty. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lates & dashboard - TemplateCombobox: new reusable combobox with debounced search, cursor pagination (25/page), keyboard nav, load-more; replaces <select> in InspectionSettingsSheet (also fixes pageSize 200->100 Zod refine bug) - /resources/template-search BFF: token-relay loader for TemplateCombobox - templates.tsx: upgraded from client-side useMemo to URL-based server-side search (?q=) with 350ms debounce; sort remains client-side - server: listTemplates() gains optional q param with LIKE filter; listTemplatesRoute schema extended with q; exactOptionalPropertyTypes fix - /resources/inspection-search BFF: full cross-all-inspections search via GET /api/inspections with search+cursor params - dashboard.tsx: searchQuery now triggers server-side BFF fetch (300ms debounce) instead of client-side bucket filter; load-more for results Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…0 data-integrity batch)
…eadiness + envelope secrets)
…ent tables, sms_mode, Twilio secret keys
…dress resolution + fan-out
…review)
Zod's `.partial()` over a field carrying `.default()` still applies the
default and injects the key. UpdateAutomationSchema derived `channels`
(and `delayMinutes`) from CreateAutomationBase, so every partial PATCH
omitting `channels` parsed to `channels: ['email']`. The service gates
on `'channels' in data`, so any such update silently dropped a tenant's
enabled SMS channel (data loss). `delayMinutes` had the same hazard via
`{...rest}`, resetting a configured delay to 0.
Fix: drop `.default()` from the base fields; re-add the defaults only on
CreateAutomationSchema. Update keeps both optional with no default, so
omitting them leaves the keys absent and the service never rewrites them.
Regression covered at the schema-parse level (service tests bypass Zod).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n SMS runtime wiring Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…SMS (migration 0026) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…_configs DDL The workers tests hand-maintain an inline CREATE TABLE tenant_configs; Task 1 (sms_mode) and Task 7 (company_phone, migration 0026) added columns the Drizzle apply path now SELECTs/writes, breaking test:workers (13/33). Mirror both columns at their schema positions. test:workers 33/33 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ent contact Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… inbound webhook, booking checkbox + email link Task 8 (Track L): - server/api/sms.ts: public opt-in resolve/confirm + two-shape Twilio inbound webhook (platform shared-number /sms/inbound + tenant-scoped /sms/inbound/:slug), signature-validated (APP_BASE_URL+path); admin attest/test-send/consent-status (requireRole owner/admin). Routers mounted in server/index.ts. - Opt-in token (Step 0 decision): NO new table — self-describing sealed payload <tenantId>~sealToken(contactId) via lib/sms/optin-token.ts (reuses the config-crypto tier-2 envelope, same as agreement tokens). - app/routes/public/sms-optin.tsx: BFF SSR double-opt-in page (DS tokens, dark-safe). - Path A: booking-form unchecked SMS opt-in checkbox -> granted (booking_form). - Path B: opt-in link injected into the booking-confirmation email at the renderer level (survives template overrides / rule disabling). - audit: sms.consent.attest + sms.test_send; route tag 'sms' allowlisted; TWILIO_* added to request Bindings; typed BFF client surface registered. - settings-automations.tsx: minimal channels[]/recipient interface alignment so the tree type-checks after the Task 6 rename (full multi-channel editor is Task 9). NOTE: inbound non-command bodies are acknowledged but NOT persisted (automation_logs automation_id/inspection_id are NOT NULL; an inbound reply binds neither; two-way surfacing is out of scope per spec §10). STOP/START consent sync is fully handled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + consent UI (Task 9)
Track L Task 9 + carried-over Task 8 review fixes:
A. Parse channels on output — AutomationService.list/create/update now return
`channels` as string[] (new private serializeRow) so the typed API surface
(AutomationSchema.channels: string[]) is truthful end to end.
B. Editor multi-channel (settings-automations.tsx) — channel checkboxes
(Email/SMS) replacing the disabled select; conditional SMS body textarea with
a live char counter + ~N segments hint; email subject/body section shown when
Email is checked; >=1 channel enforced client-side (Save disabled + hint);
save reads form.getAll("channels") + smsBody (nulled when SMS off); per-rule
+ run-log channel badge; run-log renders l.recipient. SMS placeholder palette.
C. Residual recipientEmail rename — verified no automation-log read still uses
recipientEmail (only renderer is settings-automations, already on l.recipient;
inspection-hub:737 is an unrelated payment-modal prop).
D. Settings -> Communication SMS section — sms_mode toggle + companyPhone via
extended PATCH /api/admin/tenant-config (schema+handler+GET); effective-source
line from new GET /api/admin/sms/config (resolveTwilioSource, no secrets); 3
Twilio SecretFields via existing PUT /api/admin/secrets; A2P 10DLC guidance;
inbound webhook URL (own/standalone only); Send-test-SMS.
E. Inspection-view consent + attest (inspection-hub.tsx) — ClientSmsConsent shows
granted/revoked/not-recorded via GET /sms/consent; inline "I confirm" attest
posts POST /sms/attest. BFF only.
Tests: extended settings-automations.spec (channels/recipient fixtures + sms
serialization), new settings-communication-sms + inspection-hub-sms-consent web
specs, resolveTwilioSource + list()-parse unit cases. All gates green:
unit 1604, web 361, workers 33, type-check 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…:00 UTC flakiness The previous test anchored on tomorrow's date, making the derived due-time (tomorrow@09:00Z - 1440min = today@09:00Z) only in the past after 09:00 UTC. CI jobs running before that hour would see the reminder as not-yet-due and the status would stay 'pending' instead of 'sent'. Fix: use today's date so derived due = yesterday@09:00Z, always in the past.
important-new
added a commit
to important-new/OpenInspection
that referenced
this pull request
Jun 9, 2026
…nspectorHub#134/InspectorHub#136) - inspection-hub.tsx: keep reinspectCandidates loader + destructure (Track K InspectorHub#119); remove duplicate attestSms/attesting declarations introduced by merge - _journal.json: keep migrations 0027 + 0028 (Track K) - automation-flush-sms.spec.ts: keep today-date flakiness fix from upstream/main - cmd-consumer.spec.ts + cmd-fixtures.spec.ts: keep reinspection_statuses column (Track K)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Track L — SMS notifications (#123)
Multi-channel automations engine with SMS via Twilio, consent ledger, and reminder delivery.
channels[]/sms_bodyon the automation engine; channel-aware address resolution + fan-outensureClientContact{{company_phone}}from tenant_configsGates green (unit 1606 / web 361 / workers 33) + Chrome E2E. Closes #123.
🤖 Generated with Claude Code