new_contact_created only fires when the webhook auto-creates the
contact row, so it misses the case of manually-added / imported
contacts sending their first-ever message.
first_inbound_message fires whenever the incoming message is the
contact's first customer-sent message (checked by counting prior
customer-sender messages in the conversation before insert), which
covers both auto-created and pre-existing contacts.
- Added first_inbound_message to AutomationTriggerType.
- New pill color + dropdown entry labeled "First Message from Contact".
- Webhook now counts prior customer messages before inserting the
incoming one; dispatches first_inbound_message when the count is 0.
- new_contact_created still fires for brand-new contacts; they're
independent triggers with overlapping coverage, so users pick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the /automations placeholder with a full feature:
- Migration 006 adds automations, automation_steps, automation_logs,
and automation_pending_executions with RLS matching existing tables.
- List page with active/inactive switch, quick-start templates,
duplicate, delete, and View Logs actions.
- Full-screen vertical flow builder with Trigger + Action + Condition
(Yes/No branches) + Wait cards. Inline edit forms, up/down reorder,
dotted-grid canvas.
- Execution engine (src/lib/automations/engine.ts) dispatched from the
WhatsApp webhook for new_message_received, new_contact_created, and
keyword_match. Wait steps enqueue to automation_pending_executions;
/api/automations/cron drains them behind AUTOMATION_CRON_SECRET.
- Logs viewer with per-step checkmarks and expandable detail.
Existing files touched only in webhook/route.ts (fire-and-forget
trigger dispatch) and types/index.ts (appended automation types).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause (error 23503):
broadcast_recipients.contact_id and deals.contact_id were declared
NOT NULL REFERENCES contacts(id) with no ON DELETE clause, so
Postgres defaulted to NO ACTION. Any contact who'd ever received a
broadcast or been attached to a deal became un-deletable with
"violates foreign key constraint broadcast_recipients_contact_id_fkey".
Fix (migration 004):
Relax both FKs to ON DELETE SET NULL + drop NOT NULL on both
columns. History rows survive with contact_id = NULL. The UI is
already null-safe:
- broadcast detail: contact?.name ?? 'Unknown'
- deal card: contact?.name || contact?.phone || 'No contact'
- CSV export: contact?.name ?? ''
Why not CASCADE:
CASCADE would wipe broadcast recipient rows and trigger the
aggregate recount on broadcasts (migration 003), retroactively
changing sent_count / delivered_count / read_count. It would also
delete deal history. SET NULL preserves both counts and history.
Type/code:
- Deal.contact_id + BroadcastRecipient.contact_id widen to
string | null
- DealForm: setContactId(deal.contact_id ?? "") — the contact
picker uses "" to mean unselected
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three correctness bugs were making the existing broadcast flow wrong,
not just incomplete.
1. Per-recipient variables were broken
use-broadcast-sending.ts computed templateParams per contact but shipped
only templateParams[0] to the API, so every contact in a batch received
contact 0's personalization (name/email/etc).
Fix: extend POST /api/whatsapp/broadcast to accept a per-recipient
shape `{ recipients: [{ phone, params }], ... }`. Legacy
`{ phone_numbers, template_params }` still accepted. Hook sends per-
recipient payloads; API returns whatsapp_message_id per phone.
2. Webhook never updated broadcast_recipients
handleStatusUpdate in webhook/route.ts only mirrored Meta status onto
the `messages` table. broadcast_recipients had no correlation column,
so delivered_count / read_count / replied_count never advanced for
real broadcasts.
Fix (migration 003):
- broadcast_recipients.whatsapp_message_id (+ UNIQUE index) for
Meta-message-id correlation
- composite index (broadcast_id, status) for fast aggregate scans
- AFTER INSERT/UPDATE/DELETE trigger that recomputes parent
broadcasts counts via COUNT(*) FILTER — sender + webhook only
touch the recipient row, aggregates stay consistent
Webhook:
- handleStatusUpdate also updates broadcast_recipients with forward-
only status guard (read cannot regress to sent on a replay)
- processMessage flags the replied recipient row when an inbound
message arrives from a recent broadcast contact
3. Rate limiting did not match spec
Batch 50 / no delay → Batch 10 / 1s between batches, matching the spec
and Meta's per-phone-number rate ceiling.
Also: extract statusConfig from both page components into
src/lib/broadcast-status.ts (single source of truth for PR B). Add
whatsapp_message_id to BroadcastRecipient type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebuild the Pipelines page to match spec.
Highlights:
- Drag-and-drop deal cards between stages via @dnd-kit (200ms drop animation,
emerald dashed drop zone, opacity fade while dragging)
- First-visit seed: auto-creates "Sales Pipeline" with spec-defined stages
(New Lead / Qualified / Proposal Sent / Negotiation / Won)
- Top bar: always-visible pipeline dropdown with "Manage Pipelines" option,
plus "Add Pipeline" and emerald "Add Deal" buttons
- Deal form converted from Dialog to right-side Sheet, adds assigned_to
(profile FK), "Link to Conversation" shortcut, "Mark as Won/Lost" status
buttons, and "Delete Deal"
- Pipeline settings modal: drag-to-reorder stages (@dnd-kit sortable),
per-stage color picker, "Create new pipeline" shortcut, archive
confirmation copy
- Analytics bar below board: total deals, pipeline value, avg deal size,
weighted pipeline value (probability interpolated across stages),
won/lost this month
- Contact detail sheet gains a "Deals" tab showing all deals for that
contact (value + stage badge + status)
- Migration 002: adds deals.assigned_to FK→profiles, normalizes status
CHECK to ('open','won','lost')
Note: spec called for light-gray Kanban columns (#f9fafb) + white cards
inside an otherwise-dark app. Followed spec literally; easy to retheme.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>