Track J — Automations editor (#121) + follow-up/review-request (#122), reminder trigger; enforce canonical UUID agreementId#133
Merged
important-new merged 17 commits intoJun 8, 2026
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)
…; inspection.reminder trigger; standalone cron - automations.conditions (TEXT NULL): send-time gates JSON evaluated at flush() - automations.channel (TEXT NOT NULL DEFAULT 'email'): delivery channel; SMS reserved for Track L - tenant_configs.review_url (TEXT NULL): per-company review link for Track J InspectorHub#122 - Widens trigger enum with 'inspection.reminder' (Track J D7, cron-fired) - wrangler.jsonc: adds triggers.crons ["*/5 * * * *"] so scheduled() runs on OSS deploys - Workers inline DDL synced (review_url added to both cmd-consumer + cmd-fixtures specs) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eviewUrl tenant-config field - Add `inspection.reminder` to AUTOMATION_TRIGGERS (Track J D2) - Add AUTOMATION_CHANNELS enum ['email', 'sms'] with 'email' default - Add ConditionsSchema (.strict()): requirePaid / requireSigned / serviceIds - Wire conditions (nullish) + channel (default email) into CreateAutomationSchema - AutomationSchema (response): conditions (string nullable) + channel fields - UpdateAutomationSchema inherits both via .partial() — no change needed - TenantConfigPatchSchema: reviewUrl (url, max 500, nullish, Track J InspectorHub#122) - TenantConfigGetResponseSchema data.reviewUrl (string nullable optional) - tenantConfigPatchRoute handler: maps reviewUrl → null on empty/null - tenantConfigGetRoute handler: returns reviewUrl ?? null - 3 unit tests (automation-schema.spec.ts): all pass - type-check:api: 0 errors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/update Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e, skip with reason Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…duled step (D7) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(review inactive, fail-closed) + defaultActive Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…form, run log, review-url field Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Hub#111 relaxation) Production agreements.id / inspections.id are ALWAYS crypto.randomUUID() at every create site; the Spectora import preserves external ids only for template-internal items, never as a PK. The only non-UUID ids were test seeds. So InspectorHub#111's .uuid()->.min(1) relaxation (hub send-agreement) was an unnecessary compat shim accommodating a test seed, against the project's pre-launch no-compat principle. Re-tighten to .uuid() and flip the regression test to assert non-UUID ids are rejected (canonical format enforced before launch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Summary
Gives the existing automation engine a real editor and ships two post-delivery rules, plus folds in the canonical-UUID enforcement that supersedes #132.
#121 — editor UI (
settings-automations.tsxrewrite): a When → Only-if → Do-this form editor (no flow-canvas), a run-log panel, and a review-link config field. BFF loader/action only.Conditions (D2/D4):
requirePaid/requireSigned/ service-scoping, stored as aconditionsJSON column, evaluated at send time (not trigger time) insideflush()— a delayed send is suppressed if the world changed (e.g. paid/signed during the delay). Failing a gate marks the logskippedwith a human reason.Reserved
channel(D3):email|sms, SMS greyed in the UI and defensively skipped in flush — structures the rule for a future SMS track without building it.#122 — two seeds (D5/D6): post-inspection follow-up (day 1, active) and review request (day 3, seeded inactive + fail-closed until
tenant_configs.review_urlis set). New{{review_url}}placeholder. Both seed paths updated (automation-seeds.ts+ standalone provisioning).inspection.remindertrigger (D7): the one time-relative trigger — cron-fired,delayMinutes= lead time before the inspection date, day-granular, deduped onreminder:<rule>:<inspection>. Mirrors the existing event-reminder pattern. A reminder for an inspection that later reached a terminal status (cancelled/etc.) is suppressed at send time.Migration 0024: additive
ALTER ADD COLUMNonly (automations.conditions,automations.channeldefault'email',tenant_configs.review_url) — no table rebuilds. Standalonewrangler.jsoncgains a*/5 * * * *cron so the scheduled handler actually runs on OSS deploys.Canonical UUID enforcement (supersedes #132): reverts #111's
agreementIdvalidation relaxation back toz.string().uuid()and flips the regression test to assert a non-UUID id is rejected. Pre-launch we enforce the canonical format (production ids are alwayscrypto.randomUUID()) rather than tolerate a non-UUID test fixture.Test Plan
db:checkEQUIVALENT (no drift after 0024)Closes #121
Closes #122
🤖 Generated with Claude Code