Skip to content

Track J — Automations editor (#121) + follow-up/review-request (#122), reminder trigger; enforce canonical UUID agreementId#133

Merged
important-new merged 17 commits into
InspectorHub:mainfrom
important-new:feat/track-j-automations
Jun 8, 2026
Merged

Track J — Automations editor (#121) + follow-up/review-request (#122), reminder trigger; enforce canonical UUID agreementId#133
important-new merged 17 commits into
InspectorHub:mainfrom
important-new:feat/track-j-automations

Conversation

@important-new

Copy link
Copy Markdown
Contributor

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.tsx rewrite): 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 a conditions JSON column, evaluated at send time (not trigger time) inside flush() — a delayed send is suppressed if the world changed (e.g. paid/signed during the delay). Failing a gate marks the log skipped with 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_url is set). New {{review_url}} placeholder. Both seed paths updated (automation-seeds.ts + standalone provisioning).

inspection.reminder trigger (D7): the one time-relative trigger — cron-fired, delayMinutes = lead time before the inspection date, day-granular, deduped on reminder:<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 COLUMN only (automations.conditions, automations.channel default 'email', tenant_configs.review_url) — no table rebuilds. Standalone wrangler.jsonc gains a */5 * * * * cron so the scheduled handler actually runs on OSS deploys.

Canonical UUID enforcement (supersedes #132): reverts #111's agreementId validation relaxation back to z.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 always crypto.randomUUID()) rather than tolerate a non-UUID test fixture.

Test Plan

  • type-check (app + api) 0 errors
  • lint + DS-token conformance + erasure-manifest gates pass
  • db:check EQUIVALENT (no drift after 0024)
  • unit 1556 · web 346 · workers 33 · build ✓
  • 9 new automation tests (schema / conditions+gating / reminders / seeds / editor loader+action) + UUID regression test
  • Chrome E2E on the editor: seeds present (review inactive), create rule + condition → persists, condition round-trips on re-edit, review-link save persists, run-log renders

Closes #121
Closes #122

🤖 Generated with Claude Code

important-new and others added 17 commits June 6, 2026 21:51
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>
…; 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>
@important-new important-new merged commit 1e13ab7 into InspectorHub:main Jun 8, 2026
1 check passed
@important-new important-new deleted the feat/track-j-automations branch June 8, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Automations: post-delivery follow-up & review request Automations: visual rule editor

1 participant